From 1b14c3e834ec42c97c710eb159e11fc5a597b898 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 10:20:10 +0800 Subject: [PATCH 01/30] feat(sync): enhance configuration management and platform synchronization - Updated `.gitignore` to include new configuration paths for MCP and platform files, ensuring sensitive data is not tracked. - Refactored `sync.sh` to check for MCP configurations and provide clearer instructions for setup and initialization. - Modified `backup-config.sh` to back up both MCP and platform configurations, improving data safety during sync operations. - Revised `README.md` to reflect changes in configuration structure and provide clearer setup instructions for new users. - Enhanced synchronization logic in `sync_config.py` to support auto-discovery of platforms and improved filtering of MCP servers based on platform-specific configurations. - Updated platform-specific sync scripts to accommodate the new configuration structure, ensuring compatibility across all platforms. --- .githooks/pre-push | 5 +- .gitignore | 9 +- env/templates/mcp.template.json | 9 ++ env/templates/platform.template.json | 7 + rag-gateway/src/config.ts | 71 +++++----- sync.sh | 79 +++++------ sync/README.md | 199 ++++++++++----------------- sync/backup-config.sh | 53 ++++--- sync/platforms/claude.py | 32 +++-- sync/platforms/cline.py | 8 +- sync/platforms/codebuddy.py | 14 +- sync/platforms/codex.py | 153 +++++++------------- sync/platforms/common.py | 197 +++++++++++++++++++++----- sync/platforms/continue.py | 43 +++--- sync/platforms/cursor.py | 7 +- sync/platforms/gemini.py | 25 ++-- sync/sync_all.sh | 21 ++- sync/sync_config.py | 115 ++++++++-------- 18 files changed, 548 insertions(+), 499 deletions(-) create mode 100644 env/templates/mcp.template.json create mode 100644 env/templates/platform.template.json diff --git a/.githooks/pre-push b/.githooks/pre-push index 83631de..428117c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -16,9 +16,8 @@ # # MCP-sync: # 4. sync/sync_all.sh — sync MCP server -# definitions to Cursor / Codex / Claude / Xcode, plus the CODEX SHARED -# block from env/codex/shared.toml, plus Claude Code env from -# env/claude/settings.shared.json. +# definitions from env/mcp/*.json plus platform configs from +# env/platforms/*.json to Cursor / Codex / Claude / Xcode. # # Any failure aborts the push. # diff --git a/.gitignore b/.gitignore index 3a039e7..d2cf3e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,13 @@ # skills-engineering: local machine sync config (see scripts/config.local.sh.example) skills-engineering/scripts/config.local.sh -# env/: local secrets and platform config. Copy env/config.json.example and fill in tokens. +# env/: local secrets and platform config. +# Templates in env/templates/ are committed (no secrets). +# Real configs in env/mcp/ and env/platforms/ are gitignored (contain secrets). env/config.json +env/config.json.example +env/mcp/*.json +env/platforms/*.json *__pycache__*/ @@ -12,4 +17,4 @@ env/config.json .codex/ .claude/ docs/ -skills-engineering/ios-engineer/evolution/usage \ No newline at end of file +skills-engineering/ios-engineer/evolution/usage diff --git a/env/templates/mcp.template.json b/env/templates/mcp.template.json new file mode 100644 index 0000000..35ef2df --- /dev/null +++ b/env/templates/mcp.template.json @@ -0,0 +1,9 @@ +{ + "_comment": "MCP 服务器配置模板。复制此文件到 env/mcp/.json 并填入实际值。", + "name": "my-mcp-server", + "type": "stdio", + "command": "npx", + "args": ["-y", ""], + "env": {}, + "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] +} diff --git a/env/templates/platform.template.json b/env/templates/platform.template.json new file mode 100644 index 0000000..3fa65c6 --- /dev/null +++ b/env/templates/platform.template.json @@ -0,0 +1,7 @@ +{ + "_comment": "平台配置模板。复制此文件到 env/platforms/.json,填入该平台的配置。配置字段应严格遵循该平台的官方配置规范。", + "env": { + "YOUR_API_KEY": "your-api-key-here", + "CUSTOM_BASE_URL": "https://your-endpoint.com" + } +} diff --git a/rag-gateway/src/config.ts b/rag-gateway/src/config.ts index 15c040e..4d52d4c 100644 --- a/rag-gateway/src/config.ts +++ b/rag-gateway/src/config.ts @@ -30,7 +30,7 @@ function optionalEnv(key: string, fallback: string): string { return process.env[key] ?? fallback; } -// ── env/config.json loading ───────────────────────────────────────────────── +// ── env/platforms/rag-gateway.json loading ─────────────────────────────────── /** * Apply parsed config env values to an env-like object. @@ -52,54 +52,57 @@ export function applyGatewayConfigEnv( } } -function objectRecord(value: unknown): Record { - return value !== null && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : {}; -} +/** + * Read the gateway platform config from env/platforms/rag-gateway.json. + * The file structure is: + * { "env": { "EMBEDDING_API_KEY": "...", ... } } + */ +export function gatewayEnvFromConfig(): Record { + // Resolve env/platforms/rag-gateway.json relative to this source file. + // dist/config.js → ../../env/platforms/rag-gateway.json + const configPath = resolve( + dirname(fileURLToPath(import.meta.url)), + "../../env/platforms/rag-gateway.json", + ); -export function gatewayEnvFromConfig(values: Record): Record { - const platforms = objectRecord(values.platforms); - const ragGateway = objectRecord(platforms["rag-gateway"]); - const legacyGateway = objectRecord(platforms.gateway); - return { - ...objectRecord(legacyGateway.env), - ...objectRecord(ragGateway.env), - }; -} + if (!existsSync(configPath)) { + return {}; + } -// Resolve env/config.json relative to this source file. -// dist/config.js → ../../env/config.json -const CONFIG_JSON_PATH = resolve( - dirname(fileURLToPath(import.meta.url)), - "../../env/config.json", -); + try { + const raw = readFileSync(configPath, "utf-8"); + const values: Record = JSON.parse(raw); + const env = values?.env; + return env !== null && typeof env === "object" && !Array.isArray(env) + ? (env as Record) + : {}; + } catch { + return {}; + } +} /** - * Load env/config.json and apply platforms["rag-gateway"].env keys to process.env. + * Load env/platforms/rag-gateway.json and apply env keys to process.env. * .env values take precedence (already loaded by `import "dotenv/config"` above). * Silently degrades to .env-only if the file is missing or malformed. */ -function loadConfigJson(): void { - if (!existsSync(CONFIG_JSON_PATH)) { - console.warn(`[config] env/config.json not found at ${CONFIG_JSON_PATH}; using .env only`); - return; - } +function loadGatewayConfigJson(): void { try { - const raw = readFileSync(CONFIG_JSON_PATH, "utf-8"); - const values: Record = JSON.parse(raw); - const gatewayEnv = gatewayEnvFromConfig(values); + const gatewayEnv = gatewayEnvFromConfig(); applyGatewayConfigEnv(gatewayEnv, process.env); - console.info(`[config] Loaded env/config.json platforms["rag-gateway"].env (${Object.keys(gatewayEnv).length} keys)`); + const keyCount = Object.keys(gatewayEnv).length; + if (keyCount > 0) { + console.info(`[config] Loaded env/platforms/rag-gateway.json env (${keyCount} keys)`); + } } catch (err) { - console.warn(`[config] Failed to parse env/config.json: ${(err as Error).message}; using .env only`); + console.warn(`[config] Failed to load rag-gateway config: ${(err as Error).message}; using .env only`); } } export function loadConfig(): GatewayConfig { // Phase 1: dotenv already ran at import-time (top of file). - // Phase 2: apply env/config.json defaults for any keys still unset. - loadConfigJson(); + // Phase 2: apply env/platforms/rag-gateway.json defaults for any keys still unset. + loadGatewayConfigJson(); const embeddingModel = optionalEnv("EMBEDDING_MODEL", "bge-m3"); diff --git a/sync.sh b/sync.sh index 337bd5f..7817145 100755 --- a/sync.sh +++ b/sync.sh @@ -3,19 +3,24 @@ # ai-coding-kit 一键同步脚本 # # 用法: -# bash sync.sh # 首次使用:自动检测 config,若不存在则从 example 复制并提示编辑 -# bash sync.sh --force # 强制执行同步(跳过 config 检查提示) -# bash sync.sh --init # 仅初始化 config(从 example 复制) +# bash sync.sh # 同步所有平台的 MCP 和配置 +# bash sync.sh --force # 强制执行同步(跳过检查提示) +# bash sync.sh --init # 仅初始化环境(创建模板文件) +# +# 配置文件: +# env/mcp/ — MCP 服务器定义(每个文件一个服务) +# env/platforms/ — 各平台专属配置(遵循官方规范) +# env/templates/ — 新增 MCP/平台的模板 # # 此脚本会: -# 1. 检查 env/config.json 是否存在,不存在则从 env/config.json.example 复制 +# 1. 检查 env/mcp/ 目录是否存在配置文件 # 2. 执行 sync/sync_all.sh 同步配置到各 AI 编码工具 # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONFIG_JSON="$SCRIPT_DIR/env/config.json" -CONFIG_EXAMPLE="$SCRIPT_DIR/env/config.json.example" +MCP_DIR="$SCRIPT_DIR/env/mcp" +PLATFORMS_DIR="$SCRIPT_DIR/env/platforms" # --- 颜色输出 --- RED='\033[0;31m' @@ -37,43 +42,27 @@ elif [ "${1:-}" = "--force" ]; then MODE="force" fi -# --- 初始化 config --- -init_config() { - if [ -f "$CONFIG_JSON" ]; then - echo_ok "env/config.json 已存在,跳过初始化。" - return 0 - fi +# --- 检查配置 --- +check_config() { + local has_config=false - if [ ! -f "$CONFIG_EXAMPLE" ]; then - echo_error "找不到 env/config.json.example,请确认仓库完整性。" - exit 1 + if [ -d "$MCP_DIR" ] && [ -n "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ]; then + has_config=true fi - echo_warn "env/config.json 不存在,正在从 env/config.json.example 复制..." - cp "$CONFIG_EXAMPLE" "$CONFIG_JSON" - echo_ok "已创建 env/config.json" - - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${YELLOW} 请编辑 env/config.json 填入你的 API Keys 和 MCP 配置:${NC}" - echo -e "${YELLOW}${NC}" - echo -e "${YELLOW} vim env/config.json${NC}" - echo -e "${YELLOW} code env/config.json${NC}" - echo -e "${YELLOW}${NC}" - echo -e "${YELLOW} 编辑完成后,重新运行:${NC}" - echo -e "${YELLOW}${NC}" - echo -e "${YELLOW} bash sync.sh${NC}" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - - if [ "$MODE" = "init" ]; then - exit 0 - fi - - # 交互模式:等待用户确认是否已编辑完成 - read -r -p "是否已完成编辑?(y/n) " CONFIRM - if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then - echo_warn "已取消。编辑完 env/config.json 后运行 bash sync.sh 即可同步。" + if [ "$has_config" = false ]; then + echo_warn "env/mcp/ 目录下没有 MCP 配置文件。" + echo_warn "请按照以下步骤创建配置:" + echo "" + echo -e " ${CYAN}# 1. 从模板创建 MCP 服务器配置${NC}" + echo -e " ${CYAN}cp env/templates/mcp.template.json env/mcp/my-server.json${NC}" + echo -e " ${CYAN}$EDITOR env/mcp/my-server.json${NC}" + echo "" + echo -e " ${CYAN}# 2. 从模板创建平台配置${NC}" + echo -e " ${CYAN}cp env/templates/platform.template.json env/platforms/codex.json${NC}" + echo -e " ${CYAN}$EDITOR env/platforms/codex.json${NC}" + echo "" + echo_warn "配置完成后,重新运行: bash sync.sh" exit 0 fi } @@ -104,14 +93,12 @@ run_sync() { # --- 主流程 --- echo "" echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" -echo -e "${CYAN}║ ai-coding-kit 一键同步工具 ║${NC}" +echo -e "${CYAN}║ ai-coding-kit 一键同步工具 v3.0 ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" echo "" -init_config +check_config -if [ "$MODE" = "force" ]; then +if [ "$MODE" = "force" ] || [ "$MODE" = "sync" ]; then run_sync -elif [ "$MODE" = "sync" ]; then - run_sync -fi \ No newline at end of file +fi diff --git a/sync/README.md b/sync/README.md index dac2336..6f1a49c 100644 --- a/sync/README.md +++ b/sync/README.md @@ -1,167 +1,110 @@ # sync -`sync/` renders one local config file into each host's native format. +`sync/` reads MCP server definitions and platform configs, then renders them into each platform's native format. -Canonical source: +## Canonical sources ```text -env/config.json +env/mcp/*.json — MCP server definitions (one file per server) +env/platforms/*.json — platform-specific configs (follow each platform's spec) ``` -Template: +## Setup ```bash -cp env/config.json.example env/config.json -$EDITOR env/config.json -``` - -## MCP Server Platform Filtering +# 1. Create MCP configs from template +cp env/templates/mcp.template.json env/mcp/github.json +$EDITOR env/mcp/github.json -By default every MCP server is synced to every platform. Add an optional `platforms` array to limit which platforms receive a server: +# 2. Create platform configs from template +cp env/templates/platform.template.json env/platforms/codex.json +$EDITOR env/platforms/codex.json -```json -"mcpServers": { - "XcodeBuildMCP": { - "command": "npx", - "args": ["-y", "xcodebuildmcp@latest", "mcp"], - "platforms": ["claude", "codex"] - }, - "design-handoff": { - "url": "http://localhost:8000/mcp", - "platforms": ["claude", "cline"] - }, - "github": { - "url": "https://api.githubcopilot.com/mcp/" - } -} +# 3. Sync +bash sync.sh ``` -- `"platforms": ["claude", "codex"]` — only claude and codex receive this server. -- No `platforms` field — all platforms receive this server (existing behavior preserved). - -The `platforms` key is stripped from the output; target config files never see it. +## MCP Server File Format -## Design - -The architecture is deliberately split into three layers: - -| Layer | Owner | Purpose | -|------|-------|---------| -| Source | `env/config.json` | One maintained config file: MCP catalog, shared env, and platform-specific env/config. | -| Renderer | `sync/platforms/*.py` | Converts source schema into each platform's required file format. | -| Orchestrator | `sync/sync_config.py` | Loads the source and dispatches to selected platform renderers. | -| Target | Cursor / CodeBuddy / Codex / Claude / Xcode paths | Generated or merged files; never edited as the source of truth. | - -Platform independence lives inside the single config file: +Each `env/mcp/.json`: ```json { - "platforms": { - "gateway": { "env": {} }, - "claude": { "env": {} }, - "codex": { "env": {}, "features": {}, "projects": {} } - } + "name": "my-server", + "type": "stdio", + "command": "npx", + "args": ["-y", "my-mcp-package"], + "env": {}, + "platforms": ["claude", "codex", "codebuddy"] } ``` -Values under `platforms..env` are scoped to that platform. No global `env.shared` — each platform owns its own env vars. - -## Targets - -| Target | Output | -|------|--------| -| Cursor | Replace `mcpServers` in `~/.cursor/mcp.json`, preserving other top-level keys. | -| CodeBuddy | Replace `mcpServers` in `~/.codebuddy/mcp.json`, sync models to `~/.codebuddy/models.json`, and copy skills from `~/.claude/skills/` to `~/.codebuddy/skills/`. | -| Codex CLI | `~/.codex/mcp.generated.toml` plus managed blocks in `~/.codex/config.toml`. | -| Xcode Codex | `~/Library/Developer/Xcode/CodingAssistant/codex/` with the same TOML rendering. | -| Claude Code | Replace `mcpServers` in `~/.claude.json`, preserving other top-level keys. | -| Xcode Claude Agent | Replace `mcpServers` in `~/Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json`; per-project when projects already exist. | -| Claude settings | Merge `platforms.claude.env` into `~/.claude/settings.json` `env`, preserving unrelated env keys. | -| RAG Gateway | `rag-gateway/src/config.ts` reads `platforms["rag-gateway"].env` directly from `env/config.json`; `.env` still wins at runtime. | -| Continue | Replace `mcpServers` (with SSE header compatibility mapping) and update `models` in `~/.continue/config.yaml`. | - -Codex targets are TOML because Codex config is TOML. The maintained source remains JSON; `sync_config.py` is the adapter. +- `type`: `"stdio"` (requires `command`/`args`) or `"sse"` (requires `url`/`headers`) +- `platforms`: optional filter — omit to sync to all platforms, or list specific platforms +- `env`: environment variables passed to the MCP server process -## Adding Platforms +## Platform Config Files -**Complex platforms** (custom config format, multi-file writes, or extra logic) need a renderer module: +Each `env/platforms/.json` follows that platform's **official configuration spec**: -1. Add `sync/platforms/.py` with a `sync(data) -> None` function. -2. Register it in `TARGETS` inside `sync/sync_config.py`. +| Platform | File | Follows | +|----------|------|---------| +| Codex | `codex.json` | [Codex config.toml schema](https://developers.openai.com/codex/config-reference) | +| Claude | `claude.json` | Claude Code settings.json `env` + `hooks` | +| CodeBuddy | `codebuddy.json` | CodeBuddy `models.json` schema | +| Gemini | `gemini.json` | Gemini CLI env vars | +| Continue | `continue.json` | Continue `config.yaml` models | +| Cursor | `cursor.json` | (no platform config needed) | +| Cline | `cline.json` | (no platform config needed) | +| RAG Gateway | `rag-gateway.json` | Gateway env vars | -**Simple JSON-MCP platforms** (only need `mcpServers` written to a JSON file) can be declared directly in `env/config.json` without any Python: - -```json -"platforms": { - "zed": { "type": "json-mcp", "path": "~/.config/zed/mcp.json" } -} -``` +The JSON keys map directly to the platform's native format — no field name translation needed. -`sync_config.py` auto-discovers all `type=json-mcp` entries and builds sync functions for them at runtime. Adding Zed, Kiro, or any other simple platform requires only a config change. +## Targets -Do not add another top-level sync script for each platform. The stable command should remain: +| Target | Output | +|--------|--------| +| Cursor | Replace `mcpServers` in `~/.cursor/mcp.json` | +| CodeBuddy | Replace `mcpServers` in `~/.codebuddy/mcp.json`, sync `models.json`, skills | +| Codex CLI | `~/.codex/mcp.generated.toml` + managed blocks in `config.toml` | +| Xcode Codex | `~/Library/.../CodingAssistant/codex/` | +| Claude Code | Replace `mcpServers` in `~/.claude.json` + Xcode Claude | +| Claude settings | Merge `env` + `hooks` into `~/.claude/settings.json` | +| Cline | Replace `mcpServers` in VSCode extension settings + skills sync | +| Gemini CLI | Replace `mcpServers` in `~/.gemini/settings.json` + `~/.zshrc` env | +| Continue | Update `mcpServers` + `models` in `~/.continue/config.yaml` | + +## Adding a Platform + +1. Copy template: `cp env/templates/platform.template.json env/platforms/my-platform.json` +2. Fill in config following the platform's official spec +3. If the platform only needs `mcpServers` in a JSON file, add `"mcp_target": "~/.my-platform/mcp.json"` to the config +4. If custom rendering is needed, create `sync/platforms/my_platform.py` with a `sync(mcp_servers, cfg)` function and register in `sync_config.py` + +## Adding an MCP Server ```bash -python3 sync/sync_config.py --target -``` - -This keeps orchestration, CLI flags, and missing-config behavior in one place while letting each platform own its native rendering. - -## Codex Model Provider - -`platforms.codex.modelProvider` controls whether the generated Codex TOML pins a custom provider: - -```json -"modelProvider": "custom" +cp env/templates/mcp.template.json env/mcp/my-new-server.json +$EDITOR env/mcp/my-new-server.json +bash sync.sh ``` -Generates: - -```toml -model_provider = "custom" - -[model_providers.custom] -... -``` - -Omit `modelProvider` or set it to `null` / `""` to avoid generating `model_provider` and `[model_providers.*]`. In that mode, Codex uses its own default provider/model behavior, while other shared fields such as `[features]`, `[projects.*]`, and MCP blocks still sync. - ## Commands ```bash -bash sync/sync_all.sh -``` - -Targeted runs: - -```bash -python3 sync/sync_config.py --target cursor -python3 sync/sync_config.py --target codebuddy -python3 sync/sync_config.py --target codex -python3 sync/sync_config.py --target claude -python3 sync/sync_config.py --target gemini -python3 sync/sync_config.py --target continue -``` - -## Managed Blocks - -Codex config files contain two generated regions: - -```text -# BEGIN CODEX SHARED (from env/config.json) -... -# END CODEX SHARED - -# BEGIN MCP SYNC (from env/config.json) -... -# END MCP SYNC +bash sync.sh # sync all +python3 sync/sync_config.py --target all # sync all (Python direct) +python3 sync/sync_config.py --target codex # single platform ``` -Everything outside those markers is host-specific and preserved. Keep `developer_instructions`, sandbox, plugins, Xcode-only MCP, notifications, and local overrides outside managed blocks. +## Design Principles -JSON MCP targets treat `env/config.json` as authoritative: each run replaces the target `mcpServers` object so source-side deletes and edits propagate. Non-MCP top-level keys are preserved. Platform env blocks, such as Claude settings `env`, remain merge-based so unrelated local environment keys survive. +1. **MCP separation**: one file per server — no monolithic config +2. **Platform spec compliance**: config keys match the platform's native naming exactly +3. **Zero field-name mapping**: renderers convert format (JSON→TOML, JSON→YAML), not field names +4. **Auto-discovery**: platforms are discovered from `env/platforms/` directory ## Safety -`env/config.json` is gitignored because it may contain API keys and MCP tokens. Commit only `env/config.json.example`. -keys and MCP tokens. Commit only `env/config.json.example`. +All files under `env/mcp/` and `env/platforms/` are gitignored (contain secrets). +Only `env/templates/` is committed (no secrets, placeholder values only). diff --git a/sync/backup-config.sh b/sync/backup-config.sh index ee4c548..413cd6d 100755 --- a/sync/backup-config.sh +++ b/sync/backup-config.sh @@ -1,15 +1,16 @@ #!/usr/bin/env bash -# Backup and restore env/config.json. +# Backup and restore env/mcp/ + env/platforms/ configuration. # # Usage: -# bash sync/backup-config.sh backup — create timestamped backup -# bash sync/backup-config.sh restore — restore latest backup to env/config.json +# bash sync/backup-config.sh backup — create timestamped backup of config dirs +# bash sync/backup-config.sh restore — restore latest backup # bash sync/backup-config.sh list — list backups set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_JSON="$REPO_ROOT/env/config.json" +MCP_DIR="$REPO_ROOT/env/mcp" +PLATFORMS_DIR="$REPO_ROOT/env/platforms" BACKUP_DIR="$HOME/.ai-coding-kit-backups" cmd="${1:-list}" @@ -17,19 +18,30 @@ cmd="${1:-list}" case "$cmd" in backup) mkdir -p "$BACKUP_DIR" - if [ ! -f "$CONFIG_JSON" ]; then - echo "[backup] $CONFIG_JSON does not exist — nothing to back up." >&2 - exit 1 + + has_files=false + if [ -d "$MCP_DIR" ] && [ -n "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ]; then + has_files=true + fi + if [ -d "$PLATFORMS_DIR" ] && [ -n "$(ls -A "$PLATFORMS_DIR"/*.json 2>/dev/null || true)" ]; then + has_files=true + fi + + if [ "$has_files" = false ]; then + echo "[backup] No config files found in env/mcp/ or env/platforms/ — nothing to back up." >&2 + exit 0 fi + ts="$(date +%Y%m%d_%H%M%S)" - dest="$BACKUP_DIR/config_${ts}.json" - cp "$CONFIG_JSON" "$dest" + dest="$BACKUP_DIR/config_${ts}.tar.gz" + + tar -czf "$dest" -C "$REPO_ROOT/env" mcp platforms 2>/dev/null || true chmod 600 "$dest" echo "[backup] Saved: $dest" # Keep last 10 backups, remove older ones - keep=$(ls -1t "$BACKUP_DIR"/config_*.json 2>/dev/null | head -10) - for f in "$BACKUP_DIR"/config_*.json; do + keep=$(ls -1t "$BACKUP_DIR"/config_*.tar.gz 2>/dev/null | head -10) + for f in "$BACKUP_DIR"/config_*.tar.gz; do if ! echo "$keep" | grep -qF "$f"; then rm "$f" echo "[backup] Pruned old: $f" @@ -38,28 +50,33 @@ case "$cmd" in ;; restore) - latest=$(ls -1t "$BACKUP_DIR"/config_*.json 2>/dev/null | head -1) + latest=$(ls -1t "$BACKUP_DIR"/config_*.tar.gz 2>/dev/null | head -1) if [ -z "$latest" ]; then echo "[backup] No backups found in $BACKUP_DIR" >&2 exit 1 fi - if [ -f "$CONFIG_JSON" ]; then - echo "[backup] $CONFIG_JSON already exists. Overwrite? (y/N)" + + has_existing=false + [ -d "$MCP_DIR" ] && [ -n "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ] && has_existing=true + [ -d "$PLATFORMS_DIR" ] && [ -n "$(ls -A "$PLATFORMS_DIR"/*.json 2>/dev/null || true)" ] && has_existing=true + + if [ "$has_existing" = true ]; then + echo "[backup] Existing config files found. Overwrite? (y/N)" read -r answer if [ "$answer" != "y" ] && [ "$answer" != "Y" ]; then echo "[backup] Aborted." exit 0 fi fi - cp "$latest" "$CONFIG_JSON" - chmod 600 "$CONFIG_JSON" - echo "[backup] Restored $latest → $CONFIG_JSON" + + tar -xzf "$latest" -C "$REPO_ROOT/env" + echo "[backup] Restored $latest -> env/mcp/ + env/platforms/" ;; list) if [ -d "$BACKUP_DIR" ]; then echo "Backups in $BACKUP_DIR:" - ls -1th "$BACKUP_DIR"/config_*.json 2>/dev/null || echo " (none)" + ls -1th "$BACKUP_DIR"/config_*.tar.gz 2>/dev/null || echo " (none)" else echo "No backups yet. Run: bash sync/backup-config.sh backup" fi diff --git a/sync/platforms/claude.py b/sync/platforms/claude.py index 3884cbb..a320808 100644 --- a/sync/platforms/claude.py +++ b/sync/platforms/claude.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Any -from .common import env_for_platform, mcp_servers, merge_object, platform_config, read_json_object, write_json +from .common import merge_object, read_json_object, write_json CLAUDE_JSON = Path.home() / ".claude.json" CLAUDE_SETTINGS_JSON = Path.home() / ".claude" / "settings.json" @@ -21,8 +21,7 @@ def _install_hook_scripts() -> None: print(f"Installed hook script: {dest}") -def _hooks_for_platform(data: dict[str, Any]) -> dict[str, Any]: - cfg = platform_config(data, "claude") +def _expand_hooks(cfg: dict[str, Any]) -> dict[str, Any]: raw: dict[str, Any] = cfg.get("hooks", {}) expanded: dict[str, Any] = {} for event, entries in raw.items(): @@ -39,7 +38,7 @@ def _hooks_for_platform(data: dict[str, Any]) -> dict[str, Any]: return expanded -def sync_xcode_claude_json(servers: dict[str, Any]) -> None: +def _sync_xcode_claude_json(servers: dict[str, Any]) -> None: data = read_json_object(XCODE_CLAUDE_JSON) projects = data.get("projects") if isinstance(projects, dict) and projects: @@ -54,22 +53,32 @@ def sync_xcode_claude_json(servers: dict[str, Any]) -> None: print(f"Replaced MCP servers in {XCODE_CLAUDE_JSON} ({mode}).") -def sync(data: dict[str, Any]) -> None: - servers = mcp_servers(data, "claude") +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers and Claude platform config.""" + # Write ~/.claude.json claude = read_json_object(CLAUDE_JSON) - claude["mcpServers"] = servers + claude["mcpServers"] = mcp_servers write_json(CLAUDE_JSON, claude) print(f"Replaced MCP servers in {CLAUDE_JSON} (other top-level config preserved).") - sync_xcode_claude_json(servers) + # Xcode Claude Agent + _sync_xcode_claude_json(mcp_servers) - env = env_for_platform(data, "claude") + # Write ~/.claude/settings.json settings = read_json_object(CLAUDE_SETTINGS_JSON) - settings["env"] = merge_object(settings.get("env"), env) + env = cfg.get("env", {}) + if isinstance(env, dict) and env: + settings["env"] = merge_object(settings.get("env"), env) + print(f"Merged env into {CLAUDE_SETTINGS_JSON} ({len(env)} vars; other keys preserved).") + else: + print(f"[claude] No env vars in platform config — skipping env merge.") + + # Install hook scripts _install_hook_scripts() - config_hooks = _hooks_for_platform(data) + # Merge hooks from platform config + config_hooks = _expand_hooks(cfg) if config_hooks: existing_hooks: dict[str, Any] = settings.get("hooks", {}) existing_hooks.update(config_hooks) @@ -77,4 +86,3 @@ def sync(data: dict[str, Any]) -> None: print(f"Merged hooks into {CLAUDE_SETTINGS_JSON} ({len(config_hooks)} event(s)).") write_json(CLAUDE_SETTINGS_JSON, settings) - print(f"Merged env into {CLAUDE_SETTINGS_JSON} ({len(env)} vars; other keys preserved).") diff --git a/sync/platforms/cline.py b/sync/platforms/cline.py index 298efb0..c0d020e 100644 --- a/sync/platforms/cline.py +++ b/sync/platforms/cline.py @@ -2,11 +2,10 @@ from pathlib import Path from typing import Any -from .common import mcp_servers, read_json_object, write_json +from .common import read_json_object, write_json _STORAGE_SUFFIX = "saoudrizwan.claude-dev/settings/cline_mcp_settings.json" -# Cline can be installed in any of these editors; sync to all that are present. _CANDIDATE_MCP_PATHS = [ Path.home() / f"Library/Application Support/{editor}/User/globalStorage/{_STORAGE_SUFFIX}" for editor in ("Cursor", "Code", "Code - Insiders") @@ -49,6 +48,7 @@ def _sync_skills() -> None: print(f"Synced {len(synced)} skills to {CLINE_SKILLS_DIR}: {', '.join(synced) or '(none)'}.") -def sync(data: dict[str, Any]) -> None: - _sync_mcp(mcp_servers(data, "cline")) +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers and skills to Cline (VSCode extension).""" + _sync_mcp(mcp_servers) _sync_skills() diff --git a/sync/platforms/codebuddy.py b/sync/platforms/codebuddy.py index b25f185..6832f66 100644 --- a/sync/platforms/codebuddy.py +++ b/sync/platforms/codebuddy.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any -from .common import mcp_servers, platform_config, read_json_object, sync_json_mcp, write_json +from .common import read_json_object, sync_json_mcp, write_json MCP_TARGET = Path.home() / ".codebuddy" / "mcp.json" MODELS_TARGET = Path.home() / ".codebuddy" / "models.json" @@ -10,8 +10,7 @@ CLAUDE_SKILLS_DIR = Path.home() / ".claude" / "skills" -def _sync_models(data: dict[str, Any]) -> None: - cfg = platform_config(data, "codebuddy") +def _sync_models(cfg: dict[str, Any]) -> None: models = cfg.get("models") available_models = cfg.get("availableModels") @@ -49,7 +48,8 @@ def _sync_skills() -> None: print(f"Synced {len(synced)} skills to {CODEBUDDY_SKILLS_DIR}: {', '.join(synced) or '(none)'}.") -def sync(data: dict[str, Any]) -> None: - sync_json_mcp(MCP_TARGET, mcp_servers(data, "codebuddy")) - _sync_models(data) - _sync_skills() \ No newline at end of file +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers, models, and skills to CodeBuddy.""" + sync_json_mcp(MCP_TARGET, mcp_servers) + _sync_models(cfg) + _sync_skills() diff --git a/sync/platforms/codex.py b/sync/platforms/codex.py index 72ca083..854b184 100644 --- a/sync/platforms/codex.py +++ b/sync/platforms/codex.py @@ -7,17 +7,17 @@ codex_config_path, codex_generated_toml_path, env_for_platform, - mcp_servers, - platform_config, + load_platform_config, toml_array, toml_header_key_segment, toml_inline_table, toml_quote, + toml_section, toml_value, xcode_codex_dir, ) -ZSHRC_BEGIN = "# BEGIN CODEX ENV SYNC (from env/config.json)" +ZSHRC_BEGIN = "# BEGIN CODEX ENV SYNC (from env/platforms/codex.json)" ZSHRC_END = "# END CODEX ENV SYNC" ZSHRC_BLOCK_PATTERN = re.compile( r"# BEGIN CODEX ENV SYNC(?: \(from [^)]+\))?" @@ -28,29 +28,18 @@ ) -def generate_zshrc_env_block(data: dict[str, Any]) -> str: - """Generate export lines for codex env vars, including the envKey mapping.""" - codex = platform_config(data, "codex") - env = env_for_platform(data, "codex") - env_key = codex.get("envKey", "") - - lines: list[str] = [] - for k, v in env.items(): - lines.append(f'export {k}="{v}"') - - # envKey is the var name codex's provider config reads for the API key. - # Write it explicitly if it isn't already covered by env. - if env_key and env_key not in env: - api_key = env.get("OPENAI_API_KEY", "") - if api_key: - lines.append(f'export {env_key}="{api_key}"') - +def _generate_zshrc_env_block(cfg: dict[str, Any]) -> str: + """Generate export lines for codex env vars from platform config.""" + env = env_for_platform("codex") + if not env: + return "" + lines = [f'export {k}="{v}"' for k, v in env.items()] return "\n".join(lines) -def sync_zshrc_env(data: dict[str, Any]) -> None: +def _sync_zshrc_env() -> None: """Write codex env vars into a managed block in ~/.zshrc.""" - body = generate_zshrc_env_block(data) + body = _generate_zshrc_env_block({}) # cfg not needed, env_for_platform reads from file if not body: return @@ -69,9 +58,6 @@ def sync_zshrc_env(data: dict[str, Any]) -> None: zshrc.write_text(new_text, encoding="utf-8") print(f"Updated codex env vars in {zshrc}.") - # Source ~/.zshrc so vars are active in the current subprocess environment. - # This does NOT propagate to the parent terminal; the user must run - # `source ~/.zshrc` once in their open terminal for immediate effect. try: subprocess.run(["zsh", "-c", f"source {zshrc}"], check=True, capture_output=True) print(f"Sourced {zshrc} (current process).") @@ -79,8 +65,7 @@ def sync_zshrc_env(data: dict[str, Any]) -> None: print(f"[warn] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") - -MCP_BEGIN = "# BEGIN MCP SYNC (from env/config.json)" +MCP_BEGIN = "# BEGIN MCP SYNC (from env/mcp/)" MCP_END = "# END MCP SYNC" MCP_BLOCK_PATTERN = re.compile( r"# BEGIN MCP SYNC(?: \(from [^)]+\))?" @@ -89,7 +74,7 @@ def sync_zshrc_env(data: dict[str, Any]) -> None: re.DOTALL, ) -SHARED_BEGIN = "# BEGIN CODEX SHARED (from env/config.json)" +SHARED_BEGIN = "# BEGIN CODEX SHARED (from env/platforms/codex.json)" SHARED_END = "# END CODEX SHARED" SHARED_BLOCK_PATTERN = re.compile( r"# BEGIN CODEX SHARED(?: \(from [^)]+\))?" @@ -100,7 +85,8 @@ def sync_zshrc_env(data: dict[str, Any]) -> None: def generate_mcp_toml(servers: dict[str, Any]) -> str: - lines: list[str] = ["# AUTOGENERATED from env/config.json", ""] + """Generate TOML for MCP servers (platform-agnostic).""" + lines: list[str] = ["# AUTOGENERATED from env/mcp/", ""] for name, raw_cfg in servers.items(): if not isinstance(raw_cfg, dict): raise ValueError(f"mcpServers.{name} must be an object.") @@ -109,7 +95,7 @@ def generate_mcp_toml(servers: dict[str, Any]) -> str: lines.append(f"[mcp_servers.{key}]") if "url" in cfg: lines.append(f"url = {toml_quote(str(cfg['url']))}") - if "headers" in cfg and isinstance(cfg["headers"], dict): + if "headers" in cfg and isinstance(cfg["headers"], dict) and cfg["headers"]: headers = {k: str(v) for k, v in cfg["headers"].items()} lines.append(f"headers = {toml_inline_table(headers)}") else: @@ -124,87 +110,39 @@ def generate_mcp_toml(servers: dict[str, Any]) -> str: return "\n".join(lines).rstrip() -def generate_codex_shared_toml(data: dict[str, Any]) -> str: - codex = platform_config(data, "codex") - codex_env = env_for_platform(data, "codex") - - model_provider = codex.get("modelProvider") - use_custom_provider = isinstance(model_provider, str) and model_provider != "" - - model = codex.get("model") +def generate_shared_toml(cfg: dict[str, Any]) -> str: + """Generate Codex platform TOML from platform config. - base_url = codex.get("openaiBaseUrl") or codex_env.get("OPENAI_BASE_URL") - provider_name = codex.get("providerName", model_provider or "custom") - wire_api = codex.get("wireApi", "responses") - env_key = codex.get("envKey", "OPENAI_API_KEY") + The platform config follows Codex's official config.toml schema, + so we can use toml_section() for automatic conversion. + """ + lines: list[str] = ["# AUTOGENERATED from env/platforms/codex.json"] - lines: list[str] = ["# AUTOGENERATED from env/config.json"] - if model: - lines.append(f"model = {toml_quote(str(model))}") - - # model_provider 行: provider 有效时激活,null 时注释掉 - if use_custom_provider: - lines.append(f'model_provider = {toml_quote(str(provider_name))}') - lines.append(f'preferred_auth_method = "apikey"') - else: - lines.append(f'# model_provider = ""') - lines.append(f'# preferred_auth_method = "apikey"') - - if effort := codex.get("modelReasoningEffort"): - lines.append(f"model_reasoning_effort = {toml_quote(str(effort))}") - - if personality := codex.get("personality"): - lines.append(f"personality = {toml_quote(str(personality))}") - - if (disable_response_storage := codex.get("disable_response_storage")) is not None: - lines.append(f"disable_response_storage = {'true' if disable_response_storage else 'false'}") - - if base_url: - lines.extend( - [ - "", - f"[model_providers.{toml_header_key_segment(str(provider_name))}]", - f"name = {toml_quote(str(provider_name))}", - f"base_url = {toml_quote(str(base_url))}", - f"env_key = {toml_quote(str(env_key))}", - f"wire_api = {toml_quote(str(wire_api))}", - ] - ) - - features = codex.get("features") - if isinstance(features, dict) and features: - lines.extend(["", "[features]"]) - for key, value in features.items(): - lines.append(f"{key} = {toml_value(value)}") - - projects = codex.get("projects") - if isinstance(projects, dict) and projects: - for path, cfg in projects.items(): - if not isinstance(cfg, dict): - raise ValueError(f"platforms.codex.projects.{path} must be an object.") - lines.extend(["", f"[projects.{toml_header_key_segment(str(path))}]"]) - for key, value in cfg.items(): - lines.append(f"{key} = {toml_value(value)}") + # Top-level keys (model, personality, etc.) + section = toml_section(cfg) + if section.strip(): + lines.append(section) return "\n".join(lines).rstrip() -def merge_codex_managed_blocks(cfg: Path, shared_body: str, mcp_body: str) -> None: +def merge_managed_blocks(cfg_path: Path, shared_body: str, mcp_body: str) -> None: + """Merge CODEX SHARED and MCP blocks into the target config.toml.""" shared_block = f"{SHARED_BEGIN}\n{shared_body}\n{SHARED_END}" if shared_body else "" mcp_block = f"{MCP_BEGIN}\n{mcp_body}\n{MCP_END}" region = (shared_block + "\n\n" + mcp_block) if shared_block else mcp_block region = f"\n{region}\n" - if not cfg.exists(): - cfg.parent.mkdir(parents=True, exist_ok=True) - cfg.write_text( + if not cfg_path.exists(): + cfg_path.parent.mkdir(parents=True, exist_ok=True) + cfg_path.write_text( f"# Codex config\n# Add host-specific settings outside managed blocks.\n{region}", encoding="utf-8", ) - print(f"Created {cfg} with managed blocks.") + print(f"Created {cfg_path} with managed blocks.") return - text = cfg.read_text(encoding="utf-8") + text = cfg_path.read_text(encoding="utf-8") text = SHARED_BLOCK_PATTERN.sub("", text) text = MCP_BLOCK_PATTERN.sub("", text) @@ -219,27 +157,32 @@ def merge_codex_managed_blocks(cfg: Path, shared_body: str, mcp_body: str) -> No new_text = text.rstrip() + "\n" + region action = "Appended managed blocks" - cfg.write_text(new_text, encoding="utf-8") - print(f"{action} in {cfg}.") + cfg_path.write_text(new_text, encoding="utf-8") + print(f"{action} in {cfg_path}.") -def sync(data: dict[str, Any]) -> None: - servers = mcp_servers(data, "codex") - generated = generate_mcp_toml(servers) - shared = generate_codex_shared_toml(data) +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers and Codex platform config to native TOML format.""" + generated = generate_mcp_toml(mcp_servers) + shared = generate_shared_toml(cfg) + # Write standalone mcp.generated.toml out = codex_generated_toml_path() out.parent.mkdir(parents=True, exist_ok=True) out.write_text(generated, encoding="utf-8") print(f"Wrote: {out}") - merge_codex_managed_blocks(codex_config_path(), shared, generated) + # Merge into config.toml + merge_managed_blocks(codex_config_path(), shared, generated) + + # Xcode Codex target xc = xcode_codex_dir() xc.mkdir(parents=True, exist_ok=True) xc_gen = xc / "mcp.generated.toml" xc_gen.write_text(generated, encoding="utf-8") print(f"Wrote: {xc_gen}") - merge_codex_managed_blocks(xc / "config.toml", shared, generated) + merge_managed_blocks(xc / "config.toml", shared, generated) - if platform_config(data, "codex").get("needExport"): - sync_zshrc_env(data) + # Export env vars to ~/.zshrc + if cfg.get("env"): + _sync_zshrc_env() diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 1eea4db..6726037 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -5,70 +5,112 @@ from typing import Any REPO_ROOT = Path(__file__).resolve().parents[2] -SRC_CONFIG = REPO_ROOT / "env" / "config.json" +MCP_DIR = REPO_ROOT / "env" / "mcp" +PLATFORMS_DIR = REPO_ROOT / "env" / "platforms" -def load_config() -> dict[str, Any] | None: - if not SRC_CONFIG.exists(): - print(f"[sync] {SRC_CONFIG} is missing (gitignored local file).") - print("[sync] Copy env/config.json.example -> env/config.json, edit, then run again.") - print("[sync] Skipping sync; pre-push will not block on this.") - return None +# ── Configuration loading ──────────────────────────────────────────────────── - data = json.loads(SRC_CONFIG.read_text(encoding="utf-8")) - if not isinstance(data, dict): - raise ValueError(f"{SRC_CONFIG} must contain a JSON object.") - return data +def load_all_mcp() -> dict[str, Any]: + """Scan env/mcp/*.json and return a merged dict of {server_name: server_config}. + Each file should contain: + {"name": "server-name", "type": "stdio|sse", ..., "platforms": [...]} -def object_at(data: dict[str, Any], key: str) -> dict[str, Any]: - val = data.get(key, {}) - if val is None: + Returns {} if env/mcp/ is missing or empty (graceful degradation). + """ + if not MCP_DIR.is_dir(): + print(f"[sync] {MCP_DIR} directory not found — no MCP servers loaded.") return {} - if not isinstance(val, dict): - raise ValueError(f"{key} must be an object.") - return val + + result: dict[str, Any] = {} + for f in sorted(MCP_DIR.glob("*.json")): + try: + data = json.loads(f.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + print(f"[sync] Skipping {f.name}: {exc}") + continue + if not isinstance(data, dict): + print(f"[sync] Skipping {f.name}: not a JSON object.") + continue + name = data.get("name", f.stem) + # Strip internal metadata: name, _comment + clean = {k: v for k, v in data.items() if k not in ("name", "_comment")} + result[name] = clean + return result -def platform_config(data: dict[str, Any], platform: str) -> dict[str, Any]: - platforms = object_at(data, "platforms") - cfg = platforms.get(platform, {}) - if cfg is None: +def load_platform_config(platform: str) -> dict[str, Any]: + """Load platform-specific config from env/platforms/.json. + + Returns {} if the file doesn't exist. + """ + path = PLATFORMS_DIR / f"{platform}.json" + if not path.is_file(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + print(f"[sync] Failed to load {path}: {exc}") + return {} + if not isinstance(data, dict): + print(f"[sync] {path} must contain a JSON object — skipped.") return {} - if not isinstance(cfg, dict): - raise ValueError(f"platforms.{platform} must be an object.") - return cfg + return {k: v for k, v in data.items() if k not in ("_comment",)} -def env_for_platform(data: dict[str, Any], platform: str) -> dict[str, str]: - cfg = platform_config(data, platform) +def env_for_platform(platform: str) -> dict[str, str]: + """Extract env vars from a platform config's top-level 'env' key. + + The 'env' key holds {VAR_NAME: value} pairs that the sync engine writes + to ~/.zshrc managed blocks or platform settings.json as appropriate. + """ + cfg = load_platform_config(platform) env = cfg.get("env", {}) if env is None: env = {} if not isinstance(env, dict): raise ValueError(f"platforms.{platform}.env must be an object.") - return {k: v for k, v in env.items() if isinstance(k, str) and isinstance(v, str) and v != ""} -def mcp_servers(data: dict[str, Any], platform: str | None = None) -> dict[str, Any]: - raw = data.get("mcpServers", {}) - if not isinstance(raw, dict): - raise ValueError("mcpServers must be an object.") +def filter_mcp_for_platform(mcp_all: dict[str, Any], platform: str) -> dict[str, Any]: + """Filter MCP servers to those enabled for the given platform. + + A server is included if: + - It has no 'platforms' key (included everywhere), OR + - Its 'platforms' list includes the given platform name. + + The 'platforms' key is stripped from the output. + The 'type' key is also stripped (rendering concern, not output concern). + """ result: dict[str, Any] = {} - for name, cfg in raw.items(): + for name, cfg in mcp_all.items(): if not isinstance(cfg, dict): result[name] = cfg continue allowed = cfg.get("platforms") if allowed is not None: - if not isinstance(allowed, list) or (platform is not None and platform not in allowed): + if not isinstance(allowed, list) or platform not in allowed: continue - # strip internal routing metadata before writing to target - result[name] = {k: v for k, v in cfg.items() if k != "platforms"} + result[name] = {k: v for k, v in cfg.items() if k not in ("platforms", "type")} return result +def discover_platforms() -> list[str]: + """Return platform names that have a config file in env/platforms/. + + Used by the orchestrator to auto-discover sync targets. + """ + if not PLATFORMS_DIR.is_dir(): + return [] + return sorted( + f.stem for f in PLATFORMS_DIR.glob("*.json") + ) + + +# ── JSON I/O utilities ─────────────────────────────────────────────────────── + def sync_json_mcp(path: Path, servers: dict[str, Any]) -> None: if path.is_symlink(): path.unlink() @@ -100,6 +142,8 @@ def merge_object(existing: Any, updates: dict[str, Any]) -> dict[str, Any]: return {**base, **updates} +# ── Path helpers ───────────────────────────────────────────────────────────── + def codex_config_path() -> Path: if p := os.environ.get("CODEX_CONFIG"): return Path(p).expanduser() @@ -118,6 +162,8 @@ def xcode_codex_dir() -> Path: return Path.home() / "Library/Developer/Xcode/CodingAssistant/codex" +# ── TOML generation utilities ──────────────────────────────────────────────── + def toml_quote(s: str) -> str: return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' @@ -148,3 +194,84 @@ def toml_array(items: list[Any]) -> str: def toml_inline_table(values: dict[str, Any]) -> str: return "{ " + ", ".join(f"{k} = {toml_value(v)}" for k, v in values.items()) + " }" + + +def toml_section(entries: dict[str, Any], *, ignore: set[str] | None = None) -> str: + """Convert a dict tree to TOML key-value lines and [table] sections. + + Skips keys in `ignore` (default: {'env', '_comment', 'projects', 'model_providers'}). + Nested dicts with scalar values become [parent] tables with key=value lines. + Deeper nested dicts become [parent.child] tables. + Returns a TOML string suitable for insertion into managed blocks. + """ + skip = ignore or {"env", "_comment", "projects", "model_providers"} + lines: list[str] = [] + + def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: + """Emit a TOML [table] section from a dict.""" + # Separate scalars/lists from nested dicts + sub_tables: dict[str, dict[str, Any]] = {} + has_scalars = False + for k, v in sub.items(): + if isinstance(v, dict): + sub_tables[k] = v + else: + has_scalars = True + if isinstance(v, list): + lines.append(f"{k} = {toml_value(v)}") + elif v is not None: + lines.append(f"{k} = {toml_value(v)}") + # Emit nested sub-tables + for sub_key, sub_value in sub_tables.items(): + section_key = toml_header_key_segment(f"{parent_key}.{sub_key}") + lines.append(f"[{section_key}]") + _emit_table(sub_key, sub_value) + if has_scalars or not sub_tables: + lines.append("") + + # Top-level scalars and simple values + for key, value in entries.items(): + if key in skip: + continue + if isinstance(value, dict): + # Emit as [key] table + # Check if any sub-value is itself a dict (deeper nesting) + has_deep = any(isinstance(v, dict) for v in value.values()) + if has_deep: + lines.append(f"[{key}]") + _emit_table(key, value) + else: + # Flat dict: emit as [key] with key=value lines + lines.append(f"[{key}]") + for sub_key, sub_value in value.items(): + if isinstance(sub_value, list): + lines.append(f"{sub_key} = {toml_value(sub_value)}") + elif sub_value is not None: + lines.append(f"{sub_key} = {toml_value(sub_value)}") + lines.append("") + elif isinstance(value, list): + lines.append(f"{key} = {toml_value(value)}") + elif value is not None: + lines.append(f"{key} = {toml_value(value)}") + + # model_providers section + providers = entries.get("model_providers") + if isinstance(providers, dict): + for pid, pcfg in providers.items(): + if not isinstance(pcfg, dict): + continue + lines.append(f"\n[model_providers.{toml_header_key_segment(str(pid))}]") + for k, v in pcfg.items(): + lines.append(f"{k} = {toml_value(v)}") + + # projects section + projects = entries.get("projects") + if isinstance(projects, dict): + for path, pcfg in projects.items(): + if not isinstance(pcfg, dict): + continue + lines.append(f"\n[projects.{toml_header_key_segment(str(path))}]") + for k, v in pcfg.items(): + lines.append(f"{k} = {toml_value(v)}") + + return "\n".join(lines) diff --git a/sync/platforms/continue.py b/sync/platforms/continue.py index e7cd30a..c6cffd6 100644 --- a/sync/platforms/continue.py +++ b/sync/platforms/continue.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any -from .common import mcp_servers, platform_config +from .common import load_platform_config def dump_yaml_scalar(v: Any) -> str: @@ -73,23 +73,23 @@ def dump_yaml(data: Any, indent_level: int = 0) -> str: def update_yaml_root_key(yaml_text: str, key_name: str, new_key_yaml: str) -> str: lines = yaml_text.splitlines() new_lines = [] - + in_key = False key_replaced = False - + for line in lines: stripped = line.strip() is_empty_or_comment = not stripped or stripped.startswith("#") - + is_root_key = False if not is_empty_or_comment and not line.startswith(" "): if ":" in line: is_root_key = True - + if is_root_key: if in_key: in_key = False - + curr_key = line.split(":", 1)[0].strip() if curr_key == key_name: in_key = True @@ -97,41 +97,40 @@ def update_yaml_root_key(yaml_text: str, key_name: str, new_key_yaml: str) -> st new_lines.append(new_key_yaml) key_replaced = True continue - + if in_key: continue - + new_lines.append(line) - + if not key_replaced: if new_lines and new_lines[-1].strip(): new_lines.append("") new_lines.append(new_key_yaml) - + return "\n".join(new_lines) + "\n" -def sync(data: dict[str, Any]) -> None: - cfg = platform_config(data, "continue") +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers and models to Continue (YAML format).""" path_str = cfg.get("path", "~/.continue/config.yaml") target_path = Path(path_str).expanduser() - + if not target_path.exists(): print(f"[warn] Continue configuration file does not exist at {target_path}. Skipping.") return yaml_text = target_path.read_text(encoding="utf-8") - + # 1. Sync mcpServers - servers = mcp_servers(data, "continue") continue_servers = [] - for name, srv_cfg in sorted(servers.items()): + for name, srv_cfg in sorted(mcp_servers.items()): if not isinstance(srv_cfg, dict): continue srv = {"name": name} for k, v in srv_cfg.items(): srv[k] = v - + # Continue schema mapping for SSE / Remote servers if "url" in srv: if "type" not in srv: @@ -140,16 +139,16 @@ def sync(data: dict[str, Any]) -> None: headers = srv.pop("headers") if headers: srv["requestOptions"] = {"headers": headers} - + continue_servers.append(srv) - + if not continue_servers: new_mcp_yaml = "mcpServers: []" else: new_mcp_yaml = "mcpServers:\n" + dump_yaml(continue_servers, indent_level=2) - + yaml_text = update_yaml_root_key(yaml_text, "mcpServers", new_mcp_yaml) - + # 2. Sync models (only if present in configuration) models = cfg.get("models") if models is not None: @@ -162,6 +161,6 @@ def sync(data: dict[str, Any]) -> None: new_models_yaml = "models:\n" + dump_yaml(models, indent_level=2) yaml_text = update_yaml_root_key(yaml_text, "models", new_models_yaml) print("Replaced models in Continue config.") - + target_path.write_text(yaml_text, encoding="utf-8") print(f"Replaced MCP servers in {target_path}.") diff --git a/sync/platforms/cursor.py b/sync/platforms/cursor.py index 7ed09fb..fec34fb 100644 --- a/sync/platforms/cursor.py +++ b/sync/platforms/cursor.py @@ -1,10 +1,11 @@ from pathlib import Path from typing import Any -from .common import mcp_servers, sync_json_mcp +from .common import filter_mcp_for_platform, load_all_mcp, load_platform_config, sync_json_mcp _TARGET = Path.home() / ".cursor/mcp.json" -def sync(data: dict[str, Any]) -> None: - sync_json_mcp(_TARGET, mcp_servers(data, "cursor")) +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Write MCP servers to ~/.cursor/mcp.json.""" + sync_json_mcp(_TARGET, mcp_servers) diff --git a/sync/platforms/gemini.py b/sync/platforms/gemini.py index fc5f775..668d8b2 100644 --- a/sync/platforms/gemini.py +++ b/sync/platforms/gemini.py @@ -3,11 +3,11 @@ from pathlib import Path from typing import Any -from .common import env_for_platform, mcp_servers, platform_config, sync_json_mcp +from .common import sync_json_mcp _TARGET = Path.home() / ".gemini/settings.json" -ZSHRC_BEGIN = "# BEGIN GEMINI ENV SYNC (from env/config.json)" +ZSHRC_BEGIN = "# BEGIN GEMINI ENV SYNC (from env/platforms/gemini.json)" ZSHRC_END = "# END GEMINI ENV SYNC" ZSHRC_BLOCK_PATTERN = re.compile( r"# BEGIN GEMINI ENV SYNC(?: \(from [^)]+\))?" @@ -18,12 +18,15 @@ ) -def sync_zshrc_env(data: dict[str, Any]) -> None: - env = env_for_platform(data, "gemini") - if not env: +def _sync_zshrc_env(cfg: dict[str, Any]) -> None: + env = cfg.get("env", {}) + if not isinstance(env, dict) or not env: + return + + lines = [f'export {k}="{v}"' for k, v in env.items() if isinstance(k, str) and isinstance(v, str) and v] + if not lines: return - lines = [f'export {k}="{v}"' for k, v in env.items()] block = f"{ZSHRC_BEGIN}\n" + "\n".join(lines) + f"\n{ZSHRC_END}\n" zshrc = Path.home() / ".zshrc" @@ -46,8 +49,8 @@ def sync_zshrc_env(data: dict[str, Any]) -> None: print(f"[warn] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") -def sync(data: dict[str, Any]) -> None: - sync_json_mcp(_TARGET, mcp_servers(data, "gemini")) - cfg = platform_config(data, "gemini") - if cfg.get("needExport"): - sync_zshrc_env(data) +def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: + """Sync MCP servers and env vars to Gemini CLI.""" + sync_json_mcp(_TARGET, mcp_servers) + if cfg.get("env"): + _sync_zshrc_env(cfg) diff --git a/sync/sync_all.sh b/sync/sync_all.sh index 946e156..43de090 100755 --- a/sync/sync_all.sh +++ b/sync/sync_all.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Sync configuration sources to Cursor / CodeBuddy / Codex / Claude Code / Cline / Xcode. +# Sync MCP servers and platform configs to native formats. # -# Source (sibling of this sync/ dir): -# - env/config.json — MCP catalog + platform-specific env/config, gitignored. +# Sources: +# env/mcp/*.json — MCP server definitions (platform-agnostic) +# env/platforms/*.json — platform-specific configs # # Targets: # 1) Cursor: generate ~/.cursor/mcp.json with mcpServers. @@ -18,24 +19,22 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_JSON="$REPO_ROOT/env/config.json" +MCP_DIR="$REPO_ROOT/env/mcp" -if [ ! -f "$CONFIG_JSON" ]; then - echo "[sync] $CONFIG_JSON is missing (gitignored local file)." >&2 - echo "[sync] Copy env/config.json.example -> env/config.json, edit, then run this script again." >&2 +if [ ! -d "$MCP_DIR" ] || [ -z "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ]; then + echo "[sync] No MCP config files found in $MCP_DIR." >&2 + echo "[sync] Copy env/templates/mcp.template.json -> env/mcp/.json, edit, then run again." >&2 echo "[sync] Skipping sync; pre-push will not block on this." >&2 exit 0 fi -# Auto-backup config.json before sync (keeps last 10 in ~/.ai-coding-kit-backups/) +# Auto-backup config before sync (keeps last 10 in ~/.ai-coding-kit-backups/) bash "$SCRIPT_DIR/backup-config.sh" backup echo "[1/1] Sync config to Cursor / CodeBuddy / Codex / Claude / Cline / Xcode" python3 "$SCRIPT_DIR/sync_config.py" --target all -# Source ~/.zshrc to load any env vars written by the sync (e.g. DATAEYES_API_KEY). -# This takes effect in the current script process only; to apply immediately in your -# open terminal run: source ~/.zshrc +# Source ~/.zshrc to load any env vars written by the sync. if [ -f "$HOME/.zshrc" ]; then # shellcheck disable=SC1090 source "$HOME/.zshrc" 2>/dev/null || true diff --git a/sync/sync_config.py b/sync/sync_config.py index f6cf2de..d6a38a5 100644 --- a/sync/sync_config.py +++ b/sync/sync_config.py @@ -1,32 +1,32 @@ #!/usr/bin/env python3 """ -Sync env/config.json into Cursor, Codex, Claude Code, Cline, and Xcode. +Sync MCP servers and platform configs into native formats. -The source stays outside this directory because it is runtime configuration, not -sync tool code. Platform-specific rendering lives in sync/platforms/. +Sources: + env/mcp/*.json — MCP server definitions (platform-agnostic) + env/platforms/*.json — platform-specific configs (follow each platform's spec) -Platforms with complex rendering (Claude, Codex, Cline) are registered in TARGETS. -Simple JSON-MCP platforms can be declared directly in env/config.json without code: - - "platforms": { - "zed": { "type": "json-mcp", "path": "~/.config/zed/mcp.json" } - } +Platforms are auto-discovered from env/platforms/; adding a new platform only +requires a config file and (if complex rendering is needed) a renderer module. """ import argparse -import importlib import sys from collections.abc import Callable -from pathlib import Path from typing import Any from platforms import claude, cline, codebuddy, codex, cursor, gemini -from platforms.common import load_config, mcp_servers, read_json_object, write_json +from platforms.common import discover_platforms, filter_mcp_for_platform, load_all_mcp, load_platform_config -_continue = importlib.import_module("platforms.continue") +# continue.py contains 'continue' keyword which can't be a Python import name. +import importlib as _importlib +_continue = _importlib.import_module("platforms.continue") -SyncFn = Callable[[dict[str, Any]], None] +# sync_fn signature: (mcp_servers: dict, platform_cfg: dict) -> None +SyncFn = Callable[[dict[str, Any], dict[str, Any]], None] -TARGETS: dict[str, SyncFn] = { +# Platforms that have custom renderer logic (not pure JSON-MCP). +# Registered here, discovered from env/platforms/ for pure JSON-MCP platforms. +RENDERERS: dict[str, SyncFn] = { "cursor": cursor.sync, "codebuddy": codebuddy.sync, "codex": codex.sync, @@ -37,58 +37,51 @@ } -def _build_declarative_targets(data: dict[str, Any]) -> dict[str, SyncFn]: - """Return sync functions for platforms declared as type=json-mcp in config. +def _auto_discover_targets() -> dict[str, SyncFn]: + """Build the full target map: registered renderers + auto-discovered JSON-MCP platforms.""" + all_targets: dict[str, SyncFn] = dict(RENDERERS) + discovered = discover_platforms() - Example config entry: - "platforms": { - "zed": { "type": "json-mcp", "path": "~/.config/zed/mcp.json" } - } - """ - result: dict[str, SyncFn] = {} - for name, cfg in data.get("platforms", {}).items(): - if not isinstance(cfg, dict) or cfg.get("type") != "json-mcp": - continue - path_str = cfg.get("path", "") - if not path_str: - print(f"[warn] platforms.{name} is type=json-mcp but missing 'path' — skipped.") + # Platforms that are config-only (no sync target) — skip silently + _config_only = {"rag-gateway"} + + for name in discovered: + if name in all_targets: + continue # already has a custom renderer + if name in _config_only: + continue # config-only platform, not a sync target + cfg = load_platform_config(name) + mcp_target = cfg.get("mcp_target") + if not mcp_target: + print(f"[warn] platform '{name}' has no custom renderer and no 'mcp_target' — skipped.") continue - target_path = Path(path_str).expanduser() - def make_sync(p: Path, pname: str) -> SyncFn: - def _sync(d: dict[str, Any]) -> None: - if p.is_symlink(): - p.unlink() - existing = read_json_object(p) - existing["mcpServers"] = mcp_servers(d, pname) - write_json(p, existing) - print(f"Replaced MCP servers in {p}.") + from pathlib import Path as _Path + from platforms.common import sync_json_mcp as _sync_json_mcp - return _sync + target_path = _Path(mcp_target).expanduser() - result[name] = make_sync(target_path, name) - return result + def _make_sync(p: _Path, pname: str) -> SyncFn: + def _s(mcp_servers: dict[str, Any], _platform_cfg: dict[str, Any]) -> None: + _sync_json_mcp(p, mcp_servers) + return _s + all_targets[name] = _make_sync(target_path, name) + print(f"[sync] Auto-discovered JSON-MCP platform: {name} -> {target_path}") -def _warn_orphans(data: dict[str, Any], all_targets: dict[str, SyncFn]) -> None: - """Warn about platforms that have config entries but no sync handler.""" - for name, cfg in data.get("platforms", {}).items(): - if name in all_targets: - continue - if isinstance(cfg, dict) and cfg.get("type") == "json-mcp": - continue - print(f"[warn] platforms.{name} has config but no sync handler — skipped.") + return all_targets def main() -> None: - data = load_config() - if data is None: + mcp_all = load_all_mcp() + if not mcp_all: + print("[sync] No MCP servers found in env/mcp/ — nothing to sync.") return - declarative = _build_declarative_targets(data) - all_targets: dict[str, SyncFn] = {**TARGETS, **declarative} - - _warn_orphans(data, all_targets) + all_targets = _auto_discover_targets() + if not all_targets: + print("[sync] No sync targets discovered — check env/platforms/.") + return valid = sorted(all_targets.keys()) parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) @@ -101,10 +94,16 @@ def main() -> None: args = parser.parse_args() if args.target == "all": - for sync in all_targets.values(): - sync(data) + for name in valid: + fn = all_targets[name] + mcp_servers = filter_mcp_for_platform(mcp_all, name) + platform_cfg = load_platform_config(name) + fn(mcp_servers, platform_cfg) elif args.target in all_targets: - all_targets[args.target](data) + fn = all_targets[args.target] + mcp_servers = filter_mcp_for_platform(mcp_all, args.target) + platform_cfg = load_platform_config(args.target) + fn(mcp_servers, platform_cfg) else: print(f"[error] Unknown target '{args.target}'. Valid: all, {', '.join(valid)}", file=sys.stderr) raise SystemExit(1) From 014be6f26892691b0203da38f97280b1a6b1e7f6 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 10:40:52 +0800 Subject: [PATCH 02/30] refactor(sync): streamline secrets management and configuration checks - Updated `.gitignore` to ensure only `env/secrets.json` is ignored, while MCP and platform configurations remain tracked. - Enhanced `sync.sh` to check for the existence of `env/secrets.json` and provide clear instructions for user setup. - Refactored secrets loading and resolution logic in `common.py` to support nested secret structures and improve error handling. - Revised `README.md` to clarify the configuration process and highlight the new secrets injection mechanism for platform synchronization. --- .gitignore | 9 ++-- env/mcp/apifox.json | 10 ++++ env/mcp/filesystem.json | 7 +++ env/mcp/gateway.json | 7 +++ env/mcp/github.json | 9 ++++ env/mcp/lanhu.json | 11 +++++ env/mcp/moonvy.json | 7 +++ env/mcp/playwright.json | 8 ++++ env/mcp/shell.json | 7 +++ env/mcp/xcodebuild.json | 11 +++++ env/platforms/claude.json | 24 ++++++++++ env/platforms/codebuddy.json | 34 ++++++++++++++ env/platforms/codex.json | 35 ++++++++++++++ env/platforms/continue.json | 15 ++++++ env/platforms/gemini.json | 7 +++ env/platforms/rag-gateway.json | 7 +++ env/secrets.json.example | 33 +++++++++++++ sync.sh | 68 ++++++++++++++------------- sync/README.md | 86 ++++++++++++++++++++++++++-------- sync/platforms/common.py | 78 +++++++++++++++++++++++++++++- 20 files changed, 414 insertions(+), 59 deletions(-) create mode 100644 env/mcp/apifox.json create mode 100644 env/mcp/filesystem.json create mode 100644 env/mcp/gateway.json create mode 100644 env/mcp/github.json create mode 100644 env/mcp/lanhu.json create mode 100644 env/mcp/moonvy.json create mode 100644 env/mcp/playwright.json create mode 100644 env/mcp/shell.json create mode 100644 env/mcp/xcodebuild.json create mode 100644 env/platforms/claude.json create mode 100644 env/platforms/codebuddy.json create mode 100644 env/platforms/codex.json create mode 100644 env/platforms/continue.json create mode 100644 env/platforms/gemini.json create mode 100644 env/platforms/rag-gateway.json create mode 100644 env/secrets.json.example diff --git a/.gitignore b/.gitignore index d2cf3e2..649c39b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,12 @@ # skills-engineering: local machine sync config (see scripts/config.local.sh.example) skills-engineering/scripts/config.local.sh -# env/: local secrets and platform config. -# Templates in env/templates/ are committed (no secrets). -# Real configs in env/mcp/ and env/platforms/ are gitignored (contain secrets). +# env/: only secrets.json is gitignored. +# env/mcp/*.json and env/platforms/*.json are committed (use ${VAR} references, no real secrets). +# User only needs to create env/secrets.json from env/secrets.json.example. +env/secrets.json env/config.json env/config.json.example -env/mcp/*.json -env/platforms/*.json *__pycache__*/ diff --git a/env/mcp/apifox.json b/env/mcp/apifox.json new file mode 100644 index 0000000..1daf849 --- /dev/null +++ b/env/mcp/apifox.json @@ -0,0 +1,10 @@ +{ + "name": "apifox", + "type": "stdio", + "command": "npx", + "args": ["-y", "apifox-mcp-server@latest", "--project=5440764"], + "env": { + "APIFOX_ACCESS_TOKEN": "${apifox.token}" + }, + "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] +} diff --git a/env/mcp/filesystem.json b/env/mcp/filesystem.json new file mode 100644 index 0000000..c2f6ef3 --- /dev/null +++ b/env/mcp/filesystem.json @@ -0,0 +1,7 @@ +{ + "name": "filesystem", + "type": "stdio", + "command": "/opt/homebrew/bin/npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/song/Desktop/"], + "platforms": ["claude", "codex", "continue"] +} diff --git a/env/mcp/gateway.json b/env/mcp/gateway.json new file mode 100644 index 0000000..f26ba29 --- /dev/null +++ b/env/mcp/gateway.json @@ -0,0 +1,7 @@ +{ + "name": "gateway", + "type": "sse", + "url": "http://localhost:3000/mcp/sse", + "headers": {}, + "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] +} diff --git a/env/mcp/github.json b/env/mcp/github.json new file mode 100644 index 0000000..4755a7f --- /dev/null +++ b/env/mcp/github.json @@ -0,0 +1,9 @@ +{ + "name": "github", + "type": "sse", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${github.token}" + }, + "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] +} diff --git a/env/mcp/lanhu.json b/env/mcp/lanhu.json new file mode 100644 index 0000000..6a9de1a --- /dev/null +++ b/env/mcp/lanhu.json @@ -0,0 +1,11 @@ +{ + "name": "lanhu", + "type": "stdio", + "command": "/bin/bash", + "args": ["/Users/song/Desktop/github/lanhu-mcp/run-stdio.sh"], + "env": { + "LANHU_USER_NAME": "寒江孤影", + "LANHU_USER_ROLE": "Developer" + }, + "platforms": ["claude", "codex", "cline"] +} diff --git a/env/mcp/moonvy.json b/env/mcp/moonvy.json new file mode 100644 index 0000000..761cdfb --- /dev/null +++ b/env/mcp/moonvy.json @@ -0,0 +1,7 @@ +{ + "name": "moonvy", + "type": "stdio", + "command": "node", + "args": ["/Users/song/Desktop/github/moonvy-design-mcp/server.js"], + "platforms": ["claude", "codex", "cline"] +} diff --git a/env/mcp/playwright.json b/env/mcp/playwright.json new file mode 100644 index 0000000..9709481 --- /dev/null +++ b/env/mcp/playwright.json @@ -0,0 +1,8 @@ +{ + "name": "playwright", + "type": "stdio", + "command": "npx", + "args": ["-y", "@playwright/mcp@latest", "--extension"], + "env": {}, + "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] +} diff --git a/env/mcp/shell.json b/env/mcp/shell.json new file mode 100644 index 0000000..4540155 --- /dev/null +++ b/env/mcp/shell.json @@ -0,0 +1,7 @@ +{ + "name": "shell", + "type": "stdio", + "command": "/opt/homebrew/bin/npx", + "args": ["-y", "shell-mcp-server"], + "platforms": ["claude", "codex", "continue"] +} diff --git a/env/mcp/xcodebuild.json b/env/mcp/xcodebuild.json new file mode 100644 index 0000000..d1c45c7 --- /dev/null +++ b/env/mcp/xcodebuild.json @@ -0,0 +1,11 @@ +{ + "name": "XcodeBuildMCP", + "type": "stdio", + "command": "npx", + "args": ["-y", "xcodebuildmcp@latest", "mcp"], + "env": { + "XCODEBUILDMCP_CWD": "${workspaceFolder}", + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,ui-automation,debugging,device" + }, + "platforms": ["claude", "codex"] +} diff --git a/env/platforms/claude.json b/env/platforms/claude.json new file mode 100644 index 0000000..b6dedaa --- /dev/null +++ b/env/platforms/claude.json @@ -0,0 +1,24 @@ +{ + "env": { + "ANTHROPIC_AUTH_TOKEN": "${claude.token}", + "ANTHROPIC_BASE_URL": "${claude.url}", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash", + "CLAUDE_CODE_EFFORT_LEVEL": "medium", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1" + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/xmcp-init.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/env/platforms/codebuddy.json b/env/platforms/codebuddy.json new file mode 100644 index 0000000..60ce90b --- /dev/null +++ b/env/platforms/codebuddy.json @@ -0,0 +1,34 @@ +{ + "models": [ + { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "vendor": "dataeyes", + "url": "${codebuddy.url}", + "apiKey": "${codebuddy.key}", + "maxInputTokens": 128000, + "maxOutputTokens": 8192, + "supportsToolCall": true, + "supportsImages": false, + "relatedModels": { + "lite": "deepseek-v4-flash", + "reasoning": "deepseek-v4-pro" + } + }, + { + "id": "deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "vendor": "dataeyes", + "url": "${codebuddy.url}", + "apiKey": "${codebuddy.key}", + "maxInputTokens": 128000, + "maxOutputTokens": 8192, + "supportsToolCall": true, + "supportsImages": false + } + ], + "availableModels": [ + "deepseek-v4-pro", + "deepseek-v4-flash" + ] +} diff --git a/env/platforms/codex.json b/env/platforms/codex.json new file mode 100644 index 0000000..e744922 --- /dev/null +++ b/env/platforms/codex.json @@ -0,0 +1,35 @@ +{ + "model": "gpt-5.5", + "personality": "pragmatic", + "model_provider": "dataeyes", + "model_reasoning_effort": "medium", + "history": { + "persistence": "save-all" + }, + "model_providers": { + "dataeyes": { + "base_url": "${codex.url}", + "env_key": "DATAEYES_API_KEY", + "wire_api": "responses" + } + }, + "features": { + "skills": true, + "multi_agent": true, + "hooks": true, + "shell_snapshot": true, + "unified_exec": true, + "shell_tool": true + }, + "projects": { + "~/Desktop/iOS/bajoseekios": { + "trust_level": "trusted" + }, + "~/Desktop/iOS/STBaseProject": { + "trust_level": "trusted" + } + }, + "env": { + "DATAEYES_API_KEY": "${codex.key}" + } +} diff --git a/env/platforms/continue.json b/env/platforms/continue.json new file mode 100644 index 0000000..5bf538f --- /dev/null +++ b/env/platforms/continue.json @@ -0,0 +1,15 @@ +{ + "path": "~/.continue/config.yaml", + "models": [ + { + "name": "deepseek-v4-pro", + "provider": "openai", + "model": "deepseek-v4-pro", + "apiKey": "${continue.key}", + "apiBase": "${continue.url}", + "defaultCompletionOptions": { + "maxTokens": 128000 + } + } + ] +} diff --git a/env/platforms/gemini.json b/env/platforms/gemini.json new file mode 100644 index 0000000..29394a1 --- /dev/null +++ b/env/platforms/gemini.json @@ -0,0 +1,7 @@ +{ + "env": { + "GEMINI_API_KEY": "${gemini.key}", + "GOOGLE_GEMINI_BASE_URL": "${gemini.url}", + "GEMINI_MODEL": "gemini-3.5-flash" + } +} diff --git a/env/platforms/rag-gateway.json b/env/platforms/rag-gateway.json new file mode 100644 index 0000000..4cd9a4a --- /dev/null +++ b/env/platforms/rag-gateway.json @@ -0,0 +1,7 @@ +{ + "env": { + "EMBEDDING_API_KEY": "${rag-gateway.key}", + "EMBEDDING_BASE_URL": "${rag-gateway.url}", + "EMBEDDING_MODEL": "bge-m3" + } +} diff --git a/env/secrets.json.example b/env/secrets.json.example new file mode 100644 index 0000000..f6b20b6 --- /dev/null +++ b/env/secrets.json.example @@ -0,0 +1,33 @@ +{ + "_comment": "=== 用户唯一需要配置的文件 === 复制为 env/secrets.json,每个平台填入你的 key/token 和 url。然后运行 bash sync.sh。", + "github": { + "token": "ghp_your-github-personal-access-token" + }, + "apifox": { + "token": "afxp_your-apifox-access-token" + }, + "codex": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codex-api-key" + }, + "claude": { + "url": "https://your-anthropic-proxy.example.com", + "token": "sk-your-claude-auth-token" + }, + "codebuddy": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codebuddy-api-key" + }, + "continue": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codebuddy-api-key" + }, + "gemini": { + "url": "https://your-gemini-proxy.example.com", + "key": "sk-your-gemini-api-key" + }, + "rag-gateway": { + "url": "https://your-embedding-provider.example.com/v1", + "key": "sk-your-embedding-api-key" + } +} diff --git a/sync.sh b/sync.sh index 7817145..42bd63d 100755 --- a/sync.sh +++ b/sync.sh @@ -5,22 +5,26 @@ # 用法: # bash sync.sh # 同步所有平台的 MCP 和配置 # bash sync.sh --force # 强制执行同步(跳过检查提示) -# bash sync.sh --init # 仅初始化环境(创建模板文件) # -# 配置文件: -# env/mcp/ — MCP 服务器定义(每个文件一个服务) -# env/platforms/ — 各平台专属配置(遵循官方规范) +# 配置文件(已提交到 Git,开箱即用): +# env/mcp/ — MCP 服务器定义(敏感值用 ${VAR} 占位) +# env/platforms/ — 各平台专属配置(敏感值用 ${VAR} 占位) # env/templates/ — 新增 MCP/平台的模板 # +# 用户唯一需要配置的文件: +# env/secrets.json — 填写 API Keys / Tokens +# (从 env/secrets.json.example 复制并编辑) +# # 此脚本会: -# 1. 检查 env/mcp/ 目录是否存在配置文件 +# 1. 检查 env/secrets.json 是否存在(不存在则提示创建) # 2. 执行 sync/sync_all.sh 同步配置到各 AI 编码工具 # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MCP_DIR="$SCRIPT_DIR/env/mcp" -PLATFORMS_DIR="$SCRIPT_DIR/env/platforms" +SECRETS_FILE="$SCRIPT_DIR/env/secrets.json" +SECRETS_EXAMPLE="$SCRIPT_DIR/env/secrets.json.example" # --- 颜色输出 --- RED='\033[0;31m' @@ -35,35 +39,31 @@ echo_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } echo_error() { echo -e "${RED}[ERROR]${NC} $*"; } # --- 参数解析 --- -MODE="sync" -if [ "${1:-}" = "--init" ]; then - MODE="init" -elif [ "${1:-}" = "--force" ]; then - MODE="force" +FORCE=false +if [ "${1:-}" = "--force" ]; then + FORCE=true fi -# --- 检查配置 --- -check_config() { - local has_config=false - - if [ -d "$MCP_DIR" ] && [ -n "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ]; then - has_config=true +# --- 检查 secrets.json --- +check_secrets() { + if [ ! -f "$SECRETS_FILE" ]; then + echo_error "env/secrets.json 不存在!" + echo "" + echo -e " ${CYAN}# 这是你唯一需要配置的文件:${NC}" + echo -e " ${CYAN}cp env/secrets.json.example env/secrets.json${NC}" + echo -e " ${CYAN}\$EDITOR env/secrets.json${NC}" + echo "" + echo -e " 填入你的 API Keys,然后重新运行 bash sync.sh" + exit 1 fi +} - if [ "$has_config" = false ]; then +# --- 检查 MCP 配置 --- +check_mcp() { + if [ ! -d "$MCP_DIR" ] || [ -z "$(ls -A "$MCP_DIR"/*.json 2>/dev/null || true)" ]; then echo_warn "env/mcp/ 目录下没有 MCP 配置文件。" - echo_warn "请按照以下步骤创建配置:" - echo "" - echo -e " ${CYAN}# 1. 从模板创建 MCP 服务器配置${NC}" - echo -e " ${CYAN}cp env/templates/mcp.template.json env/mcp/my-server.json${NC}" - echo -e " ${CYAN}$EDITOR env/mcp/my-server.json${NC}" - echo "" - echo -e " ${CYAN}# 2. 从模板创建平台配置${NC}" - echo -e " ${CYAN}cp env/templates/platform.template.json env/platforms/codex.json${NC}" - echo -e " ${CYAN}$EDITOR env/platforms/codex.json${NC}" - echo "" - echo_warn "配置完成后,重新运行: bash sync.sh" - exit 0 + echo_warn "MCP 配置文件已包含在仓库中 — 请确认 env/mcp/*.json 存在。" + exit 1 fi } @@ -97,8 +97,12 @@ echo -e "${CYAN}║ ai-coding-kit 一键同步工具 v3.0 ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" echo "" -check_config +check_secrets +check_mcp -if [ "$MODE" = "force" ] || [ "$MODE" = "sync" ]; then +if [ "$FORCE" = true ]; then + run_sync +else + echo_step "env/secrets.json 已就绪,即将同步配置..." run_sync fi diff --git a/sync/README.md b/sync/README.md index 6f1a49c..9d1a699 100644 --- a/sync/README.md +++ b/sync/README.md @@ -1,27 +1,68 @@ # sync -`sync/` reads MCP server definitions and platform configs, then renders them into each platform's native format. +`sync/` reads MCP server definitions and platform configs, **injects secrets** from `env/secrets.json`, then renders them into each platform's native format. -## Canonical sources +## 快速开始(3 步) + +```bash +# 1. 复制 secrets 模板(唯一需要创建的文件) +cp env/secrets.json.example env/secrets.json + +# 2. 编辑填写你的 API Keys +$EDITOR env/secrets.json + +# 3. 一键同步到所有平台 +bash sync.sh +``` + +## 架构 ```text -env/mcp/*.json — MCP server definitions (one file per server) -env/platforms/*.json — platform-specific configs (follow each platform's spec) +env/ +├── secrets.json ← 你唯一需要配置的文件(gitignored) +├── secrets.json.example ← 模板(已提交,列出所有需要的 Key) +│ +├── mcp/ ← MCP 服务器定义(已提交,开箱即用) +│ ├── github.json ← token 用 ${github.token} 占位 +│ ├── apifox.json +│ └── ... +│ +├── platforms/ ← 平台配置(已提交,开箱即用) +│ ├── codex.json ← url/key 用 ${codex.url}/${codex.key} 占位 +│ ├── claude.json +│ └── ... +│ +└── templates/ ← 模板(供新增 MCP/平台时参考) + ├── mcp.template.json + └── platform.template.json ``` -## Setup +## 占位符机制 -```bash -# 1. Create MCP configs from template -cp env/templates/mcp.template.json env/mcp/github.json -$EDITOR env/mcp/github.json +所有配置文件的敏感值使用 `${platform.field}` 占位,同步时从 `env/secrets.json` 注入: -# 2. Create platform configs from template -cp env/templates/platform.template.json env/platforms/codex.json -$EDITOR env/platforms/codex.json +```json +// env/mcp/github.json(已提交) +{ "headers": { "Authorization": "Bearer ${github.token}" } } + +// env/platforms/codex.json(已提交) +{ "model_providers": { "dataeyes": { + "base_url": "${codex.url}", + "env_key": "DATAEYES_API_KEY" + }}, + "env": { "DATAEYES_API_KEY": "${codex.key}" } +} -# 3. Sync -bash sync.sh +// env/secrets.json(不提交,用户填写 — 每个平台一个对象) +{ + "github": { "token": "ghp_xxx" }, + "codex": { "url": "https://api.example.com/v1", "key": "sk-xxx" }, + ... +} + +// 运行时解析为: +{ "headers": { "Authorization": "Bearer ghp_xxx" } } +{ "base_url": "https://api.example.com/v1", ... } ``` ## MCP Server File Format @@ -42,6 +83,7 @@ Each `env/mcp/.json`: - `type`: `"stdio"` (requires `command`/`args`) or `"sse"` (requires `url`/`headers`) - `platforms`: optional filter — omit to sync to all platforms, or list specific platforms - `env`: environment variables passed to the MCP server process +- Secrets: use `${platform.field}` syntax, resolved from nested `env/secrets.json` at sync time ## Platform Config Files @@ -99,12 +141,16 @@ python3 sync/sync_config.py --target codex # single platform ## Design Principles -1. **MCP separation**: one file per server — no monolithic config -2. **Platform spec compliance**: config keys match the platform's native naming exactly -3. **Zero field-name mapping**: renderers convert format (JSON→TOML, JSON→YAML), not field names -4. **Auto-discovery**: platforms are discovered from `env/platforms/` directory +1. **One file to configure**: user only edits `env/secrets.json` — each platform has its own `{url, key/token}` object +2. **MCP separation**: one file per server — no monolithic config +3. **Platform spec compliance**: config keys match the platform's native naming exactly +4. **Zero field-name mapping**: renderers convert format (JSON→TOML, JSON→YAML), not field names +5. **Auto-discovery**: platforms are discovered from `env/platforms/` directory +6. **Secrets injection**: `${platform.field}` references are resolved from nested `env/secrets.json` at sync time ## Safety -All files under `env/mcp/` and `env/platforms/` are gitignored (contain secrets). -Only `env/templates/` is committed (no secrets, placeholder values only). +- `env/secrets.json` is **gitignored** — never committed +- `env/mcp/*.json` and `env/platforms/*.json` are **committed** — use `${VAR}` placeholders, no real secrets +- `env/secrets.json.example` is **committed** — shows required keys with placeholder values +- `env/templates/` is **committed** — templates for adding new servers/platforms diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 6726037..f0142c3 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -7,22 +7,92 @@ REPO_ROOT = Path(__file__).resolve().parents[2] MCP_DIR = REPO_ROOT / "env" / "mcp" PLATFORMS_DIR = REPO_ROOT / "env" / "platforms" +SECRETS_PATH = REPO_ROOT / "env" / "secrets.json" + +_SECRET_REF_RE = re.compile(r'\$\{([^}]+)\}') + + +# ── Secrets resolution ─────────────────────────────────────────────────────── + +def _flatten_secrets(data: dict[str, Any], prefix: str = "") -> dict[str, str]: + """Recursively flatten nested dict into {prefix.key: str_value} entries. + + Skips _comment keys at any level. + Example: {"codex": {"url": "https://...", "key": "sk-..."}} + -> {"codex.url": "https://...", "codex.key": "sk-..."} + """ + flat: dict[str, str] = {} + for k, v in data.items(): + if k.startswith("_"): + continue + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + flat.update(_flatten_secrets(v, full_key)) + elif isinstance(v, (str, int, float)): + flat[full_key] = str(v) + elif isinstance(v, list): + flat[full_key] = json.dumps(v, ensure_ascii=False) + return flat + + +def load_secrets() -> dict[str, str]: + """Load secrets from env/secrets.json. + + Returns a flat dot-notation dict, e.g. {"github.token": "...", "codex.url": "...", "codex.key": "..."}. + Supports per-platform nested format: {"codex": {"url": "...", "key": "..."}} + + If secrets.json doesn't exist, returns {} and prints a warning. + """ + if not SECRETS_PATH.is_file(): + print("[sync] ⚠ env/secrets.json not found — copy env/secrets.json.example and fill in your keys.") + return {} + try: + data = json.loads(SECRETS_PATH.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError) as exc: + print(f"[sync] Failed to load secrets: {exc}") + return {} + if not isinstance(data, dict): + return {} + return _flatten_secrets(data) + + +def resolve_secrets(data: Any, secrets: dict[str, str]) -> Any: + """Recursively resolve ${VAR} references in strings using secrets dict. + + Walks dicts, lists, and strings. Non-string values are returned as-is. + Each "${VAR}" occurrence in any string is replaced by secrets[VAR]. + If VAR is not found in secrets, the placeholder is left unchanged. + """ + if isinstance(data, str): + def _replacer(m: re.Match[str]) -> str: + key = m.group(1) + if key in secrets: + return secrets[key] + return m.group(0) + return _SECRET_REF_RE.sub(_replacer, data) + if isinstance(data, dict): + return {k: resolve_secrets(v, secrets) for k, v in data.items()} + if isinstance(data, list): + return [resolve_secrets(v, secrets) for v in data] + return data # ── Configuration loading ──────────────────────────────────────────────────── def load_all_mcp() -> dict[str, Any]: - """Scan env/mcp/*.json and return a merged dict of {server_name: server_config}. + """Scan env/mcp/*.json, resolve secrets, and return {server_name: server_config}. Each file should contain: {"name": "server-name", "type": "stdio|sse", ..., "platforms": [...]} + Secrets (${VAR}) are resolved from env/secrets.json before returning. Returns {} if env/mcp/ is missing or empty (graceful degradation). """ if not MCP_DIR.is_dir(): print(f"[sync] {MCP_DIR} directory not found — no MCP servers loaded.") return {} + secrets = load_secrets() result: dict[str, Any] = {} for f in sorted(MCP_DIR.glob("*.json")): try: @@ -33,8 +103,9 @@ def load_all_mcp() -> dict[str, Any]: if not isinstance(data, dict): print(f"[sync] Skipping {f.name}: not a JSON object.") continue + # Resolve secrets before stripping metadata + data = resolve_secrets(data, secrets) name = data.get("name", f.stem) - # Strip internal metadata: name, _comment clean = {k: v for k, v in data.items() if k not in ("name", "_comment")} result[name] = clean return result @@ -43,6 +114,7 @@ def load_all_mcp() -> dict[str, Any]: def load_platform_config(platform: str) -> dict[str, Any]: """Load platform-specific config from env/platforms/.json. + Secrets (${VAR}) are resolved from env/secrets.json before returning. Returns {} if the file doesn't exist. """ path = PLATFORMS_DIR / f"{platform}.json" @@ -56,6 +128,8 @@ def load_platform_config(platform: str) -> dict[str, Any]: if not isinstance(data, dict): print(f"[sync] {path} must contain a JSON object — skipped.") return {} + secrets = load_secrets() + data = resolve_secrets(data, secrets) return {k: v for k, v in data.items() if k not in ("_comment",)} From bcbbd86c557f835c5982ac81d6d76b4d6308f122 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 11:05:48 +0800 Subject: [PATCH 03/30] feat: restructure config, add sub-READMEs, use ~/ paths - Replace hardcoded /Users/song paths with ~/ in MCP configs (filesystem, lanhu, moonvy) - Add README.md to env/, rag-gateway/, hooks/, .githooks/ directories - Restructure root README.md as hub linking to subdirectory documentation - Remove dead docs/ links from root README --- .githooks/README.md | 39 +++++ .gitignore | 2 - README.md | 106 ++++-------- env/README.md | 70 ++++++++ env/config.json.example | 158 ------------------ env/mcp/apifox.json | 27 ++- env/mcp/filesystem.json | 18 +- env/mcp/gateway.json | 17 +- env/mcp/github.json | 21 ++- env/mcp/lanhu.json | 24 ++- env/mcp/moonvy.json | 16 +- env/mcp/playwright.json | 23 ++- env/mcp/shell.json | 17 +- env/mcp/xcodebuild.json | 25 ++- env/platforms/claude.json | 42 ++--- env/platforms/codebuddy.json | 64 +++---- env/platforms/codex.json | 62 +++---- env/platforms/continue.json | 26 +-- env/platforms/gemini.json | 10 +- env/platforms/rag-gateway.json | 10 +- env/secrets.json.example | 62 +++---- env/templates/mcp.template.json | 24 ++- env/templates/platform.template.json | 10 +- hooks/README.md | 23 +++ rag-gateway/README.md | 56 +++++++ skills-engineering/README.md | 2 +- .../ios-engineer/references/mcp_control.md | 2 +- sync/platforms/common.py | 2 +- 28 files changed, 511 insertions(+), 447 deletions(-) create mode 100644 .githooks/README.md create mode 100644 env/README.md delete mode 100644 env/config.json.example create mode 100644 hooks/README.md create mode 100644 rag-gateway/README.md diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..e6c56ed --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,39 @@ +# .githooks + +Git 钩子目录,通过 `install-hooks.sh` 注册为仓库的 `core.hooksPath`。 + +## 安装 + +```bash +bash install-hooks.sh +``` + +会将 `core.hooksPath` 指向此目录,启用以下守卫。 + +## pre-commit — 规则变更必须绑定治理记录 + +拦截对以下文件的未治理变更: +- `skills-engineering/ios-engineer/SKILL.md` +- `skills-engineering/ios-engineer/references/*.md` + +如果这些文件被 staged,同一个 commit 必须包含对应的 proposal 和 approval 记录。 + +## pre-push — 推送前强制同步并校验 + +推送前顺序执行: +1. `sync-skills.sh` — 同步 skill 到各 Agent 目录 +2. `sync-agent-preamble.sh` — 重写 preamble 托管块 +3. `verify-sync.sh` — 校验同步结果 +4. `sync_all.sh` — 同步 MCP 配置到所有平台 + +任一步骤失败则阻止推送。 + +## 紧急绕过 + +```bash +SKILL_BYPASS=1 git commit -m "..." # 跳过 skill 治理检查 +SKILL_BYPASS=1 git push # 跳过 skill-sync 段 +git push --no-verify # 跳过所有 hooks +``` + +绕过仅限紧急修复,需在 commit message 中说明原因。 diff --git a/.gitignore b/.gitignore index 649c39b..dc83eff 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ skills-engineering/scripts/config.local.sh # env/mcp/*.json and env/platforms/*.json are committed (use ${VAR} references, no real secrets). # User only needs to create env/secrets.json from env/secrets.json.example. env/secrets.json -env/config.json -env/config.json.example *__pycache__*/ diff --git a/README.md b/README.md index 007f7ec..c2909bd 100644 --- a/README.md +++ b/README.md @@ -3,40 +3,41 @@ [![Agent Skills](https://img.shields.io/badge/Agent%20Skills-8%2B%20AI%20Coding%20Tools-5856D6)](skills-engineering/README.md) [![iOS Engineer Skill](https://img.shields.io/badge/iOS%20Engineer-Swift%20%7C%20SwiftUI%20%7C%20UIKit-0A84FF)](skills-engineering/ios-engineer/SKILL.md) [![MCP Config Sync](https://img.shields.io/badge/MCP%20Config-8%20Platforms-663399)](sync/README.md) -[![Universal RAG Gateway](https://img.shields.io/badge/Universal%20RAG%20Gateway-TypeScript%20%7C%20Fastify-34C759)](docs/universal-rag-gateway.md) +[![Universal RAG Gateway](https://img.shields.io/badge/Universal%20RAG%20Gateway-TypeScript%20%7C%20Fastify-34C759)](rag-gateway/README.md) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) > **One kit. All your AI coding tools.** Agent Skills management, MCP configuration sync, iOS engineering rules, and a Universal RAG Gateway — unified for Cursor, CodeBuddy, Codex, Claude Code, Gemini CLI, Continue, Cline, and Xcode Coding Assistant. -**ai-coding-kit** is a local-first AI coding workflow toolkit for developers who use multiple AI coding tools and need a single source of truth for Agent Skills (AI coding skills / coding agent skills / prompt engineering rules), MCP server configuration (Model Context Protocol config), platform settings, and RAG Gateway routing. It replaces scattered config files with one maintainable `env/config.json` and auto-syncs to every AI coding host you use. +**ai-coding-kit** is a local-first AI coding workflow toolkit. Define your MCP servers, API keys, Agent Skills, and platform settings once — auto-sync to every AI coding host you use. -中文定位:这是一个面向 AI Coding / Agentic Coding / MCP(模型上下文协议)/ RAG Gateway 的本地工程化工具包。为同时使用多个 AI 编码工具(Cursor、CodeBuddy、Codex、Claude Code、Gemini CLI、Continue、Cline、Xcode Coding Assistant)的开发者提供统一的 Agent Skill 维护、MCP 配置同步、iOS 工程规则和智能网关路由。 +面向 AI Coding / Agentic Coding / MCP(模型上下文协议)的多工具本地工程化工具包。为同时使用多个 AI 编码工具的开发者提供统一的 Agent Skill 维护、MCP 配置同步、iOS 工程规则和智能网关路由。 -## Why ai-coding-kit? - -Managing MCP servers, API keys, and Agent Skills across multiple AI coding assistants is a hassle — each tool stores its config in a different format and location. Update one, forget the rest. **ai-coding-kit** solves this: +## Quick Start -- **One config file** → auto-generates Cursor mcp.json, Codex TOML, Claude JSON, CodeBuddy models, Continue YAML, and more. -- **One sync command** → `bash sync.sh` updates every tool in seconds. -- **Git-ignored secrets** → `env/config.json` stays local; only the sanitized example is committed. +```bash +git clone https://github.com/i-stack/ai-coding-kit.git +cd ai-coding-kit -## Features +# 唯一需要编辑的文件 +cp env/secrets.json.example env/secrets.json +$EDITOR env/secrets.json -> **Agent Skills & Prompt Engineering** -> -> Ready-to-use AI coding skills for engineering discipline, iOS / Swift / SwiftUI / UIKit development, problem analysis, logical reasoning, and cognitive expansion. Version-controlled, syncable, and auditable. +# 一键同步 +bash sync.sh +``` -> **MCP Config Sync (Model Context Protocol)** -> -> Define MCP servers once in `env/config.json` and auto-render to Cursor (mcp.json), CodeBuddy (mcp.json + models.json), Codex (config.toml), Claude Code (.claude.json + settings.json), Gemini CLI, Continue (config.yaml), and Cline — with per-server platform filtering. +## 模块 -> **iOS Engineering Rules** -> -> Production-grade rules for Swift, SwiftUI, UIKit, Xcode, concurrency (async/await, actors), networking, performance, testing, code review, migration, and release-risk control — designed for AI coding assistants to produce reliable iOS code. +各模块有独立的 README,按需深入: -> **Universal RAG Gateway** -> -> TypeScript / Fastify gateway with OpenAI-compatible API, provider routing, semantic memory, transcript storage, declarative tools, GraphRAG, and telemetry. A local alternative to cloud RAG services. +| 模块 | 说明 | 文档 | +|------|------|------| +| **skills-engineering/** | Agent Skill 内容源、多端同步、受控演进 | [README](skills-engineering/README.md) | +| **sync/** | MCP 配置同步引擎,注入 secrets 渲染到各平台原生格式 | [README](sync/README.md) | +| **env/** | 配置数据源(secrets + MCP 定义 + 平台配置) | [README](env/README.md) | +| **rag-gateway/** | TypeScript / Fastify 通用 RAG 网关(OpenAI 兼容 API) | [README](rag-gateway/README.md) | +| **hooks/** | 项目钩子脚本(xmcp 初始化等) | [README](hooks/README.md) | +| **.githooks/** | Git 提交/推送守卫(pre-commit + pre-push) | [README](.githooks/README.md) | ## Supported AI Coding Tools @@ -51,69 +52,24 @@ Managing MCP servers, API keys, and Agent Skills across multiple AI coding assis | **Cline** (VSCode) | MCP settings JSON, `skills/` | | **Xcode Coding Assistant** | Codex + Claude Agent config paths | -## Quick Start +## 安装 Git 钩子 ```bash -# 1. Clone the repository -git clone https://github.com/i-stack/ai-coding-kit.git -cd ai-coding-kit - -# 2. Configure your MCP servers, API keys, and platform settings -# First run auto-copies from the example template -vim env/config.json - -# 3. One-command sync to all your AI coding tools -bash sync.sh +bash install-hooks.sh ``` -`sync.sh` automatically syncs your MCP server config and platform settings to **Cursor, CodeBuddy, Codex, Claude Code, Xcode, Cline, Gemini CLI, and Continue** in one shot. - -| Next Steps | Documentation | -|------|---------------| -| Learn Agent Skills in depth | [skills-engineering/README.md](skills-engineering/README.md) | -| Understand MCP config sync | [sync/README.md](sync/README.md) | -| See the config template | [env/config.json.example](env/config.json.example) | -| Explore the iOS engineer skill | [skills-engineering/ios-engineer/SKILL.md](skills-engineering/ios-engineer/SKILL.md) | -| Study the RAG Gateway | [docs/universal-rag-gateway.md](docs/universal-rag-gateway.md) | -| Set up Git hooks | [install-hooks.sh](install-hooks.sh) | +启用 pre-commit(规则变更治理)和 pre-push(推送前强制同步校验)。详见 [.githooks/README.md](.githooks/README.md)。 -## What is MCP? (Model Context Protocol) +## What is MCP? [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open protocol that lets AI coding tools connect to external services — GitHub, Playwright, databases, APIs, design tools — through a standardized interface. **ai-coding-kit** gives you one place to define all your MCP servers and syncs them to every tool that supports MCP. -## Documentation - -| Topic | Link | -|------|------| -| Agent Skill engineering | [skills-engineering/README.md](skills-engineering/README.md) | -| iOS engineer skill | [skills-engineering/ios-engineer/SKILL.md](skills-engineering/ios-engineer/SKILL.md) | -| iOS skill rule index | [skills-engineering/ios-engineer/references/rule_index.md](skills-engineering/ios-engineer/references/rule_index.md) | -| Cognitive expansion skill | [skills-engineering/cognitive-expansion/SKILL.md](skills-engineering/cognitive-expansion/SKILL.md) | -| Engineering discipline skill | [skills-engineering/engineering-discipline/SKILL.md](skills-engineering/engineering-discipline/SKILL.md) | -| Logical reasoning skill | [skills-engineering/logical-reasoning/SKILL.md](skills-engineering/logical-reasoning/SKILL.md) | -| Epistemic integrity skill | [skills-engineering/epistemic-integrity/SKILL.md](skills-engineering/epistemic-integrity/SKILL.md) | -| Problem analysis skill | [skills-engineering/problem-analysis/SKILL.md](skills-engineering/problem-analysis/SKILL.md) | -| MCP and platform config sync | [sync/README.md](sync/README.md) | -| Universal RAG Gateway | [docs/universal-rag-gateway.md](docs/universal-rag-gateway.md) | -| Token comparison notes | [docs/token-comparison-results-v2.md](docs/token-comparison-results-v2.md) | - -## Repository Layout - -| Path | Role | -|------|------| -| [skills-engineering/](skills-engineering/) | Agent Skill sources, references, sync scripts, validation data, and skill evolution workflow. | -| [sync/](sync/) | Renderers and orchestration for local MCP / platform config sync. | -| [env/](env/) | Local configuration template; the real `env/config.json` is intentionally gitignored. | -| [rag-gateway/](rag-gateway/) | Universal RAG Gateway source, tests, providers, retrieval, memory, telemetry, and declarative tool runtime. | -| [docs/](docs/) | Architecture notes, Gateway status, and token comparison reports. | -| [.githooks/](.githooks/) | Repository-managed commit and push guards. | - ## Who This Is For -- **Developers using multiple AI coding tools** who want one config to rule them all — define MCP servers and Agent Skills once, sync everywhere. -- **iOS / Swift engineers** who want production-grade AI coding rules for Swift, SwiftUI, UIKit, Xcode, concurrency, testing, code review, and app migration. -- **AI infrastructure builders** experimenting with local memory, semantic retrieval, declarative tools, provider routing, and OpenAI-compatible RAG gateway patterns. -- **Team maintainers** who need a single source of truth for MCP configuration, API keys, and model settings across Cursor, CodeBuddy, Claude Code, Codex, Gemini, Continue, Cline, and Xcode. +- **Developers using multiple AI coding tools** — define MCP servers and Agent Skills once, sync everywhere. +- **iOS / Swift engineers** — production-grade AI coding rules for Swift, SwiftUI, UIKit, concurrency, testing, and migration. +- **AI infrastructure builders** — local memory, semantic retrieval, declarative tools, and OpenAI-compatible RAG gateway patterns. +- **Team maintainers** — single source of truth for MCP configuration, API keys, and model settings. ## License diff --git a/env/README.md b/env/README.md new file mode 100644 index 0000000..01641f2 --- /dev/null +++ b/env/README.md @@ -0,0 +1,70 @@ +# env + +配置数据源目录,`sync/` 引擎从此处读取所有平台和 MCP 服务器的定义。 + +## 目录结构 + +```text +env/ +├── secrets.json ← 你唯一需要填写的文件(gitignored) +├── secrets.json.example ← 模板(已提交) +│ +├── mcp/ ← MCP 服务器定义 +│ ├── github.json +│ ├── apifox.json +│ ├── filesystem.json +│ ├── playwright.json +│ ├── shell.json +│ ├── xcodebuild.json +│ ├── lanhu.json +│ ├── moonvy.json +│ └── gateway.json +│ +├── platforms/ ← 平台专属配置 +│ ├── claude.json +│ ├── codex.json +│ ├── codebuddy.json +│ ├── continue.json +│ ├── gemini.json +│ └── rag-gateway.json +│ +└── templates/ ← 新增 MCP/平台的参考模板 + ├── mcp.template.json + └── platform.template.json +``` + +## secrets.json + +嵌套结构,每个平台一个对象: + +```json +{ + "github": { "token": "ghp_xxx" }, + "codex": { "url": "https://api.example.com/v1", "key": "sk-xxx" }, + "claude": { "token": "sk-ant-xxx" }, + ... +} +``` + +新增平台时只需在此文件中追加对应的 `{url, key/token}` 即可。 + +## 占位符机制 + +所有 `mcp/` 和 `platforms/` 下的配置使用 `${platform.field}` 语法引用 secrets: + +```json +// env/mcp/github.json +{ "headers": { "Authorization": "Bearer ${github.token}" } } + +// env/platforms/codex.json +{ "base_url": "${codex.url}", "env": { "DATAEYES_API_KEY": "${codex.key}" } } +``` + +同步时由 `sync/platforms/common.py` 自动替换为真实值。 + +## 模板 + +- `templates/mcp.template.json` — 新增 MCP 服务器时复制并填写 +- `templates/platform.template.json` — 新增平台时复制并填写 + +详见 [sync/README.md](../sync/README.md)。 diff --git a/env/config.json.example b/env/config.json.example deleted file mode 100644 index d565c56..0000000 --- a/env/config.json.example +++ /dev/null @@ -1,158 +0,0 @@ -{ - "mcpServers": { - "code-collaboration": { - "command": "npx", - "args": ["-y", "@github/github-mcp-server"], - "env": { - "GITHUB_TOKEN": "YOUR_GITHUB_PAT" - } - }, - "browser-automation": { - "command": "npx", - "args": ["-y", "@playwright/mcp@latest", "--extension"], - "env": {}, - "platforms": ["claude", "codex", "cline", "continue"] - }, - "design-handoff": { - "url": "http://localhost:8000/mcp?role=Developer&name=YourName", - "platforms": ["claude", "cline"] - }, - "api-documentation": { - "command": "npx", - "args": [ - "-y", - "apifox-mcp-server@latest", - "--project=YOUR_APIFOX_PROJECT_ID" - ], - "env": { - "APIFOX_ACCESS_TOKEN": "YOUR_APIFOX_TOKEN" - }, - "platforms": ["claude", "codex", "cline"] - } - }, - "platforms": { - "rag-gateway": { - "env": { - "OPENAI_DEFAULT_MODEL": "" - } - }, - "claude": { - "env": { - "ANTHROPIC_API_KEY": "", - "ANTHROPIC_BASE_URL": "", - "ANTHROPIC_MODEL": "", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "", - "CLAUDE_CODE_EFFORT_LEVEL": "" - }, - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "~/.claude/hooks/xmcp-init.sh", - "timeout": 10 - } - ] - } - ] - } - }, - "codex": { - "model": "", - "personality": "", - "modelProvider": null, - "modelReasoningEffort": "medium", - "providerName": "", - "wireApi": "responses", - "openaiBaseUrl": "", - "envKey": "OPENAI_API_KEY", - "needExport": true, - "env": { - "OPENAI_API_KEY": "YOUR_API_KEY" - }, - "features": { - "skills": true, - "multi_agent": true, - "js_repl": false, - "default_mode_request_user_input": true - }, - "projects": { - "~/Desktop/iOS/bajoseekios": { - "trust_level": "trusted" - }, - "~/Desktop/iOS/STBaseProject": { - "trust_level": "trusted" - } - } - }, - "codebuddy": { - "needExport": true, - "env": { - "DEEP_SEEK_KEY": "" - }, - "models": [ - { - "id": "deepseek-v4-pro", - "name": "DeepSeek V4 Pro", - "vendor": "DeepSeek", - "url": "https://cloud.dataeyes.ai/v1", - "apiKey": "YOUR_API_KEY", - "maxInputTokens": 128000, - "maxOutputTokens": 8192, - "supportsToolCall": true, - "supportsImages": false, - "relatedModels": { - "lite": "deepseek-v4-flash", - "reasoning": "deepseek-v4-pro" - } - }, - { - "id": "deepseek-v4-flash", - "name": "DeepSeek V4 Flash", - "vendor": "DeepSeek", - "url": "https://cloud.dataeyes.ai/v1", - "apiKey": "YOUR_API_KEY", - "maxInputTokens": 128000, - "maxOutputTokens": 8192, - "supportsToolCall": true, - "supportsImages": false - } - ], - "availableModels": [ - "deepseek-v4-pro", - "deepseek-v4-flash" - ] - }, - "gemini": { - "needExport": true, - "env": { - "GEMINI_API_KEY": "", - "GOOGLE_GEMINI_BASE_URL": "", - "GEMINI_MODEL": "" - } - }, - "continue": { - "path": "~/.continue/config.yaml", - "models": [ - { - "name": "deepseek-v4-pro", - "provider": "openai", - "model": "deepseek-v4-pro", - "apiKey": "YOUR_API_KEY", - "apiBase": "https://api.openai.com/v1", - "defaultCompletionOptions": { - "maxTokens": 128000 - } - } - ] - }, - "_comment_json_mcp": "Declare simple platforms here without writing Python. type=json-mcp replaces mcpServers in the target JSON file.", - "zed": { - "type": "json-mcp", - "path": "~/.config/zed/mcp.json" - } - } -} diff --git a/env/mcp/apifox.json b/env/mcp/apifox.json index 1daf849..3b5814a 100644 --- a/env/mcp/apifox.json +++ b/env/mcp/apifox.json @@ -1,10 +1,21 @@ { - "name": "apifox", - "type": "stdio", - "command": "npx", - "args": ["-y", "apifox-mcp-server@latest", "--project=5440764"], - "env": { - "APIFOX_ACCESS_TOKEN": "${apifox.token}" - }, - "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] + "name": "apifox", + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "apifox-mcp-server@latest", + "--project=5440764" + ], + "env": { + "APIFOX_ACCESS_TOKEN": "${apifox.token}" + }, + "platforms": [ + "claude", + "codex", + "codebuddy", + "gemini", + "cline", + "continue" + ] } diff --git a/env/mcp/filesystem.json b/env/mcp/filesystem.json index c2f6ef3..ceaa13d 100644 --- a/env/mcp/filesystem.json +++ b/env/mcp/filesystem.json @@ -1,7 +1,15 @@ { - "name": "filesystem", - "type": "stdio", - "command": "/opt/homebrew/bin/npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/song/Desktop/"], - "platforms": ["claude", "codex", "continue"] + "name": "filesystem", + "type": "stdio", + "command": "/opt/homebrew/bin/npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "~/Desktop/" + ], + "platforms": [ + "claude", + "codex", + "continue" + ] } diff --git a/env/mcp/gateway.json b/env/mcp/gateway.json index f26ba29..ece6713 100644 --- a/env/mcp/gateway.json +++ b/env/mcp/gateway.json @@ -1,7 +1,14 @@ { - "name": "gateway", - "type": "sse", - "url": "http://localhost:3000/mcp/sse", - "headers": {}, - "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] + "name": "gateway", + "type": "sse", + "url": "http://localhost:3000/mcp/sse", + "headers": {}, + "platforms": [ + "claude", + "codex", + "codebuddy", + "gemini", + "cline", + "continue" + ] } diff --git a/env/mcp/github.json b/env/mcp/github.json index 4755a7f..a8f0660 100644 --- a/env/mcp/github.json +++ b/env/mcp/github.json @@ -1,9 +1,16 @@ { - "name": "github", - "type": "sse", - "url": "https://api.githubcopilot.com/mcp/", - "headers": { - "Authorization": "Bearer ${github.token}" - }, - "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] + "name": "github", + "type": "sse", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${github.token}" + }, + "platforms": [ + "claude", + "codex", + "codebuddy", + "gemini", + "cline", + "continue" + ] } diff --git a/env/mcp/lanhu.json b/env/mcp/lanhu.json index 6a9de1a..a603d06 100644 --- a/env/mcp/lanhu.json +++ b/env/mcp/lanhu.json @@ -1,11 +1,17 @@ { - "name": "lanhu", - "type": "stdio", - "command": "/bin/bash", - "args": ["/Users/song/Desktop/github/lanhu-mcp/run-stdio.sh"], - "env": { - "LANHU_USER_NAME": "寒江孤影", - "LANHU_USER_ROLE": "Developer" - }, - "platforms": ["claude", "codex", "cline"] + "name": "lanhu", + "type": "stdio", + "command": "/bin/bash", + "args": [ + "~/Desktop/github/lanhu-mcp/run-stdio.sh" + ], + "env": { + "LANHU_USER_NAME": "寒江孤影", + "LANHU_USER_ROLE": "Developer" + }, + "platforms": [ + "claude", + "codex", + "cline" + ] } diff --git a/env/mcp/moonvy.json b/env/mcp/moonvy.json index 761cdfb..ab04de2 100644 --- a/env/mcp/moonvy.json +++ b/env/mcp/moonvy.json @@ -1,7 +1,13 @@ { - "name": "moonvy", - "type": "stdio", - "command": "node", - "args": ["/Users/song/Desktop/github/moonvy-design-mcp/server.js"], - "platforms": ["claude", "codex", "cline"] + "name": "moonvy", + "type": "stdio", + "command": "node", + "args": [ + "~/Desktop/github/moonvy-design-mcp/server.js" + ], + "platforms": [ + "claude", + "codex", + "cline" + ] } diff --git a/env/mcp/playwright.json b/env/mcp/playwright.json index 9709481..0d44e71 100644 --- a/env/mcp/playwright.json +++ b/env/mcp/playwright.json @@ -1,8 +1,19 @@ { - "name": "playwright", - "type": "stdio", - "command": "npx", - "args": ["-y", "@playwright/mcp@latest", "--extension"], - "env": {}, - "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] + "name": "playwright", + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest", + "--extension" + ], + "env": {}, + "platforms": [ + "claude", + "codex", + "codebuddy", + "gemini", + "cline", + "continue" + ] } diff --git a/env/mcp/shell.json b/env/mcp/shell.json index 4540155..5348069 100644 --- a/env/mcp/shell.json +++ b/env/mcp/shell.json @@ -1,7 +1,14 @@ { - "name": "shell", - "type": "stdio", - "command": "/opt/homebrew/bin/npx", - "args": ["-y", "shell-mcp-server"], - "platforms": ["claude", "codex", "continue"] + "name": "shell", + "type": "stdio", + "command": "/opt/homebrew/bin/npx", + "args": [ + "-y", + "shell-mcp-server" + ], + "platforms": [ + "claude", + "codex", + "continue" + ] } diff --git a/env/mcp/xcodebuild.json b/env/mcp/xcodebuild.json index d1c45c7..a43d261 100644 --- a/env/mcp/xcodebuild.json +++ b/env/mcp/xcodebuild.json @@ -1,11 +1,18 @@ { - "name": "XcodeBuildMCP", - "type": "stdio", - "command": "npx", - "args": ["-y", "xcodebuildmcp@latest", "mcp"], - "env": { - "XCODEBUILDMCP_CWD": "${workspaceFolder}", - "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,ui-automation,debugging,device" - }, - "platforms": ["claude", "codex"] + "name": "XcodeBuildMCP", + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "xcodebuildmcp@latest", + "mcp" + ], + "env": { + "XCODEBUILDMCP_CWD": "${workspaceFolder}", + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,ui-automation,debugging,device" + }, + "platforms": [ + "claude", + "codex" + ] } diff --git a/env/platforms/claude.json b/env/platforms/claude.json index b6dedaa..a9f5646 100644 --- a/env/platforms/claude.json +++ b/env/platforms/claude.json @@ -1,24 +1,24 @@ { - "env": { - "ANTHROPIC_AUTH_TOKEN": "${claude.token}", - "ANTHROPIC_BASE_URL": "${claude.url}", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash", - "CLAUDE_CODE_EFFORT_LEVEL": "medium", - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1" - }, - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "~/.claude/hooks/xmcp-init.sh", - "timeout": 10 - } + "env": { + "ANTHROPIC_AUTH_TOKEN": "${claude.token}", + "ANTHROPIC_BASE_URL": "${claude.url}", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash", + "CLAUDE_CODE_EFFORT_LEVEL": "medium", + "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1" + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/xmcp-init.sh", + "timeout": 10 + } + ] + } ] - } - ] - } + } } diff --git a/env/platforms/codebuddy.json b/env/platforms/codebuddy.json index 60ce90b..12eb14a 100644 --- a/env/platforms/codebuddy.json +++ b/env/platforms/codebuddy.json @@ -1,34 +1,34 @@ { - "models": [ - { - "id": "deepseek-v4-pro", - "name": "DeepSeek V4 Pro", - "vendor": "dataeyes", - "url": "${codebuddy.url}", - "apiKey": "${codebuddy.key}", - "maxInputTokens": 128000, - "maxOutputTokens": 8192, - "supportsToolCall": true, - "supportsImages": false, - "relatedModels": { - "lite": "deepseek-v4-flash", - "reasoning": "deepseek-v4-pro" - } - }, - { - "id": "deepseek-v4-flash", - "name": "DeepSeek V4 Flash", - "vendor": "dataeyes", - "url": "${codebuddy.url}", - "apiKey": "${codebuddy.key}", - "maxInputTokens": 128000, - "maxOutputTokens": 8192, - "supportsToolCall": true, - "supportsImages": false - } - ], - "availableModels": [ - "deepseek-v4-pro", - "deepseek-v4-flash" - ] + "models": [ + { + "id": "deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "vendor": "dataeyes", + "url": "${codebuddy.url}", + "apiKey": "${codebuddy.key}", + "maxInputTokens": 128000, + "maxOutputTokens": 8192, + "supportsToolCall": true, + "supportsImages": false, + "relatedModels": { + "lite": "deepseek-v4-flash", + "reasoning": "deepseek-v4-pro" + } + }, + { + "id": "deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "vendor": "dataeyes", + "url": "${codebuddy.url}", + "apiKey": "${codebuddy.key}", + "maxInputTokens": 128000, + "maxOutputTokens": 8192, + "supportsToolCall": true, + "supportsImages": false + } + ], + "availableModels": [ + "deepseek-v4-pro", + "deepseek-v4-flash" + ] } diff --git a/env/platforms/codex.json b/env/platforms/codex.json index e744922..4d66ef1 100644 --- a/env/platforms/codex.json +++ b/env/platforms/codex.json @@ -1,35 +1,35 @@ { - "model": "gpt-5.5", - "personality": "pragmatic", - "model_provider": "dataeyes", - "model_reasoning_effort": "medium", - "history": { - "persistence": "save-all" - }, - "model_providers": { - "dataeyes": { - "base_url": "${codex.url}", - "env_key": "DATAEYES_API_KEY", - "wire_api": "responses" - } - }, - "features": { - "skills": true, - "multi_agent": true, - "hooks": true, - "shell_snapshot": true, - "unified_exec": true, - "shell_tool": true - }, - "projects": { - "~/Desktop/iOS/bajoseekios": { - "trust_level": "trusted" + "model": "gpt-5.5", + "personality": "pragmatic", + "model_provider": "dataeyes", + "model_reasoning_effort": "medium", + "history": { + "persistence": "save-all" + }, + "model_providers": { + "dataeyes": { + "base_url": "${codex.url}", + "env_key": "DATAEYES_API_KEY", + "wire_api": "responses" + } + }, + "features": { + "skills": true, + "multi_agent": true, + "hooks": true, + "shell_snapshot": true, + "unified_exec": true, + "shell_tool": true + }, + "projects": { + "~/Desktop/iOS/bajoseekios": { + "trust_level": "trusted" + }, + "~/Desktop/iOS/STBaseProject": { + "trust_level": "trusted" + } }, - "~/Desktop/iOS/STBaseProject": { - "trust_level": "trusted" + "env": { + "DATAEYES_API_KEY": "${codex.key}" } - }, - "env": { - "DATAEYES_API_KEY": "${codex.key}" - } } diff --git a/env/platforms/continue.json b/env/platforms/continue.json index 5bf538f..fdbb15e 100644 --- a/env/platforms/continue.json +++ b/env/platforms/continue.json @@ -1,15 +1,15 @@ { - "path": "~/.continue/config.yaml", - "models": [ - { - "name": "deepseek-v4-pro", - "provider": "openai", - "model": "deepseek-v4-pro", - "apiKey": "${continue.key}", - "apiBase": "${continue.url}", - "defaultCompletionOptions": { - "maxTokens": 128000 - } - } - ] + "path": "~/.continue/config.yaml", + "models": [ + { + "name": "deepseek-v4-pro", + "provider": "openai", + "model": "deepseek-v4-pro", + "apiKey": "${continue.key}", + "apiBase": "${continue.url}", + "defaultCompletionOptions": { + "maxTokens": 128000 + } + } + ] } diff --git a/env/platforms/gemini.json b/env/platforms/gemini.json index 29394a1..48cadf8 100644 --- a/env/platforms/gemini.json +++ b/env/platforms/gemini.json @@ -1,7 +1,7 @@ { - "env": { - "GEMINI_API_KEY": "${gemini.key}", - "GOOGLE_GEMINI_BASE_URL": "${gemini.url}", - "GEMINI_MODEL": "gemini-3.5-flash" - } + "env": { + "GEMINI_API_KEY": "${gemini.key}", + "GOOGLE_GEMINI_BASE_URL": "${gemini.url}", + "GEMINI_MODEL": "gemini-3.5-flash" + } } diff --git a/env/platforms/rag-gateway.json b/env/platforms/rag-gateway.json index 4cd9a4a..bf819e7 100644 --- a/env/platforms/rag-gateway.json +++ b/env/platforms/rag-gateway.json @@ -1,7 +1,7 @@ { - "env": { - "EMBEDDING_API_KEY": "${rag-gateway.key}", - "EMBEDDING_BASE_URL": "${rag-gateway.url}", - "EMBEDDING_MODEL": "bge-m3" - } + "env": { + "EMBEDDING_API_KEY": "${rag-gateway.key}", + "EMBEDDING_BASE_URL": "${rag-gateway.url}", + "EMBEDDING_MODEL": "bge-m3" + } } diff --git a/env/secrets.json.example b/env/secrets.json.example index f6b20b6..3ee5df9 100644 --- a/env/secrets.json.example +++ b/env/secrets.json.example @@ -1,33 +1,33 @@ { - "_comment": "=== 用户唯一需要配置的文件 === 复制为 env/secrets.json,每个平台填入你的 key/token 和 url。然后运行 bash sync.sh。", - "github": { - "token": "ghp_your-github-personal-access-token" - }, - "apifox": { - "token": "afxp_your-apifox-access-token" - }, - "codex": { - "url": "https://your-model-provider.example.com/v1", - "key": "sk-your-codex-api-key" - }, - "claude": { - "url": "https://your-anthropic-proxy.example.com", - "token": "sk-your-claude-auth-token" - }, - "codebuddy": { - "url": "https://your-model-provider.example.com/v1", - "key": "sk-your-codebuddy-api-key" - }, - "continue": { - "url": "https://your-model-provider.example.com/v1", - "key": "sk-your-codebuddy-api-key" - }, - "gemini": { - "url": "https://your-gemini-proxy.example.com", - "key": "sk-your-gemini-api-key" - }, - "rag-gateway": { - "url": "https://your-embedding-provider.example.com/v1", - "key": "sk-your-embedding-api-key" - } + "_comment": "=== 用户唯一需要配置的文件 === 复制为 env/secrets.json,每个平台填入你的 key/token 和 url。然后运行 bash sync.sh。", + "github": { + "token": "ghp_your-github-personal-access-token" + }, + "apifox": { + "token": "afxp_your-apifox-access-token" + }, + "codex": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codex-api-key" + }, + "claude": { + "url": "https://your-anthropic-proxy.example.com", + "token": "sk-your-claude-auth-token" + }, + "codebuddy": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codebuddy-api-key" + }, + "continue": { + "url": "https://your-model-provider.example.com/v1", + "key": "sk-your-codebuddy-api-key" + }, + "gemini": { + "url": "https://your-gemini-proxy.example.com", + "key": "sk-your-gemini-api-key" + }, + "rag-gateway": { + "url": "https://your-embedding-provider.example.com/v1", + "key": "sk-your-embedding-api-key" + } } diff --git a/env/templates/mcp.template.json b/env/templates/mcp.template.json index 35ef2df..90d996e 100644 --- a/env/templates/mcp.template.json +++ b/env/templates/mcp.template.json @@ -1,9 +1,19 @@ { - "_comment": "MCP 服务器配置模板。复制此文件到 env/mcp/.json 并填入实际值。", - "name": "my-mcp-server", - "type": "stdio", - "command": "npx", - "args": ["-y", ""], - "env": {}, - "platforms": ["claude", "codex", "codebuddy", "gemini", "cline", "continue"] + "_comment": "MCP 服务器配置模板。复制到 env/mcp/.json,填入实际值。敏感值使用 ${platform.field} 占位,同步时从 env/secrets.json 注入。", + "name": "my-mcp-server", + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "" + ], + "env": {}, + "platforms": [ + "claude", + "codex", + "codebuddy", + "gemini", + "cline", + "continue" + ] } diff --git a/env/templates/platform.template.json b/env/templates/platform.template.json index 3fa65c6..060985e 100644 --- a/env/templates/platform.template.json +++ b/env/templates/platform.template.json @@ -1,7 +1,7 @@ { - "_comment": "平台配置模板。复制此文件到 env/platforms/.json,填入该平台的配置。配置字段应严格遵循该平台的官方配置规范。", - "env": { - "YOUR_API_KEY": "your-api-key-here", - "CUSTOM_BASE_URL": "https://your-endpoint.com" - } + "_comment": "平台配置模板。复制到 env/platforms/.json,填入该平台配置。敏感值使用 ${platform.field} 占位,同步时从 env/secrets.json 注入。", + "env": { + "YOUR_ENV_VAR": "${your-platform.key}", + "YOUR_BASE_URL": "${your-platform.url}" + } } diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..dd983d5 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,23 @@ +# hooks + +项目级钩子脚本目录。 + +## xmcp-init.sh + +自动检测当前目录是否为 Xcode 项目,如果是则创建 `.xcodebuildmcp/config.yaml` 配置文件。 + +```bash +bash hooks/xmcp-init.sh +``` + +脚本会: +- 检测 `.xcworkspace` 判断是否为 Xcode 项目 +- 自动发现可用 iPhone 模拟器(优先 iPhone 16) +- 生成包含 workspace、scheme、simulator 名称/UDID 的 `config.yaml` + +## 与 .githooks 的区别 + +| 目录 | 用途 | +|------|------| +| `hooks/` | 项目功能钩子脚本(如 xmcp 初始化) | +| [.githooks/](../.githooks/) | Git 钩子(pre-commit / pre-push 守卫) | diff --git a/rag-gateway/README.md b/rag-gateway/README.md new file mode 100644 index 0000000..16ef7e9 --- /dev/null +++ b/rag-gateway/README.md @@ -0,0 +1,56 @@ +# rag-gateway + +基于 TypeScript / Fastify 的通用 RAG(检索增强生成)网关,提供 OpenAI 兼容 API,可作为本地 RAG 服务的自托管替代方案。 + +## 核心能力 + +- **OpenAI 兼容 API** — 标准 `/v1/chat/completions` 接口,可接入任何 OpenAI 客户端 +- **语义记忆** — 基于 Qdrant 向量数据库的持久化对话记忆检索 +- **GraphRAG** — 实体关系图,从对话中提取结构化知识 +- **声明式工具系统** — 通过 `tools.json` 定义工具,无需编写代码即可扩展 +- **MCP 接口** — 通过 SSE 暴露 `tools/list` 和 `tools/call` + +## 快速开始 + +```bash +cd rag-gateway +cp .env.example .env +# 编辑 .env 填写 embedding API key / Qdrant URL 等 +$EDITOR .env +npm install +npm run dev +``` + +## 目录结构 + +```text +rag-gateway/ +├── src/ +│ ├── index.ts ← Fastify 服务器入口 +│ ├── config.ts ← 配置加载(.env + rag-gateway.json) +│ ├── db/ ← PostgreSQL 持久化(转录、图谱) +│ ├── vector/ ← Embedding + Qdrant 向量存储 +│ ├── entity/ ← 实体关系提取 +│ ├── tool/ ← 声明式工具注册与执行 +│ ├── mcp/ ← MCP 服务器与客户端 +│ └── metrics.ts ← 指标收集 +├── tests/ ← Vitest 单元/集成测试 +├── scripts/ ← 调试和数据填充脚本 +├── tools.json ← 声明式工具定义 +└── .env.example ← 环境变量模板 +``` + +## 环境变量 + +| 变量 | 说明 | +|------|------| +| `GATEWAY_PORT` | 服务端口(默认 3000) | +| `EMBEDDING_API_KEY` | Embedding 服务 API Key | +| `EMBEDDING_BASE_URL` | Embedding 服务地址 | +| `EMBEDDING_MODEL` | Embedding 模型名称 | +| `EXTRACTION_LLM_API_KEY` | 实体提取用 LLM Key | +| `POSTGRES_URL` | PostgreSQL 连接串(可选) | +| `QDRANT_URL` | Qdrant 向量库地址(可选) | +| `GRAPHRAG_ENABLED` | 是否启用 GraphRAG(`true`/`false`) | + +配置也可从 `env/platforms/rag-gateway.json` 自动加载默认值(`.env` 优先)。 diff --git a/skills-engineering/README.md b/skills-engineering/README.md index 55c0179..b49b19b 100644 --- a/skills-engineering/README.md +++ b/skills-engineering/README.md @@ -373,7 +373,7 @@ bash install-hooks.sh 4. `sync/sync_all.sh` —— 把 MCP / Codex 共享配置同步到 Cursor / Codex / Claude / Xcode(来自 `sync/` subtree,与本守卫并存)。 任何一步失败都会 `exit 1` 并阻止 `git push`,保证远端指向的版本与本地 Agent 正在加载的版本一致。 -例外:若仅缺少本地 `env/config.json`,`sync/sync_all.sh` 会按“未配置本地密钥文件”处理并退出 `0`,即跳过本次 MCP 同步但不阻断 push。 +例外:若仅缺少本地 `env/secrets.json`,`sync/sync_all.sh` 会按"未配置本地密钥文件"处理并退出 `0`,即跳过本次 MCP 同步但不阻断 push。 ### 紧急绕过 diff --git a/skills-engineering/ios-engineer/references/mcp_control.md b/skills-engineering/ios-engineer/references/mcp_control.md index 987ef37..9772b37 100644 --- a/skills-engineering/ios-engineer/references/mcp_control.md +++ b/skills-engineering/ios-engineer/references/mcp_control.md @@ -56,7 +56,7 @@ - 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 ## iOS 场景 MCP 优先映射 -iOS 工程任务涉及构建 / 接口契约 / 设计稿对照 / 仓库取证时,**优先调用对应 MCP**,不要直接拼裸命令或肉眼比对。MCP 列表以宿主当前注入工具为准(Claude Code / Cursor / Codex 通过 `env/config.json` 同步,详见仓库 [env/](../../../env/) 数据目录与 [sync/](../../../sync/) 工具目录);当前 iOS 工程相关条目: +iOS 工程任务涉及构建 / 接口契约 / 设计稿对照 / 仓库取证时,**优先调用对应 MCP**,不要直接拼裸命令或肉眼比对。MCP 列表以宿主当前注入工具为准(Claude Code / Cursor / Codex 通过 `env/secrets.json` + `env/mcp/*.json` + `env/platforms/*.json` 同步,详见仓库 [env/](../../../env/) 数据目录与 [sync/](../../../sync/) 工具目录);当前 iOS 工程相关条目: | 场景 | 优先 MCP | 替代的常见做法 | 触发关键词 | |------|---------|---------------|-----------| diff --git a/sync/platforms/common.py b/sync/platforms/common.py index f0142c3..7156239 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -208,7 +208,7 @@ def read_json_object(path: Path) -> dict[str, Any]: def write_json(path: Path, data: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + path.write_text(json.dumps(data, indent=4, ensure_ascii=False) + "\n", encoding="utf-8") def merge_object(existing: Any, updates: dict[str, Any]) -> dict[str, Any]: From 3048fd7909a3ad69c16c379046e581d48cbdf096 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 11:32:46 +0800 Subject: [PATCH 04/30] feat(config): enhance codex configuration and synchronization features - Added new configuration options in codex.json for model verbosity, reasoning summary, and sandbox mode. - Introduced max_bytes limit for history persistence and enabled network access for sandbox workspace. - Updated sync.py to conditionally export environment variables to ~/.zshrc based on the new export_env_to_zshrc flag. - Modified common.py to include export_env_to_zshrc in the TOML serialization skip list, ensuring proper configuration management. --- env/platforms/codex.json | 31 +++++++++++++++++++++++++++++-- sync/platforms/codex.py | 4 ++-- sync/platforms/common.py | 15 ++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/env/platforms/codex.json b/env/platforms/codex.json index 4d66ef1..d2d81a6 100644 --- a/env/platforms/codex.json +++ b/env/platforms/codex.json @@ -3,8 +3,30 @@ "personality": "pragmatic", "model_provider": "dataeyes", "model_reasoning_effort": "medium", + "model_verbosity": "medium", + "model_reasoning_summary": "auto", + "plan_mode_reasoning_effort": "medium", + "hide_agent_reasoning": true, + "sandbox_mode": "workspace-write", + "approval_policy": "on-request", + "default_permissions": ":workspace", + "web_search": "cached", + "file_opener": "cursor", "history": { - "persistence": "save-all" + "persistence": "save-all", + "max_bytes": 104857600 + }, + "sandbox_workspace_write": { + "network_access": true + }, + "tools": { + "view_image": true + }, + "shell_environment_policy": { + "inherit": "all" + }, + "tui": { + "notifications": true }, "model_providers": { "dataeyes": { @@ -19,7 +41,11 @@ "hooks": true, "shell_snapshot": true, "unified_exec": true, - "shell_tool": true + "shell_tool": true, + "memories": true, + "personality": true, + "enable_request_compression": true, + "skill_mcp_dependency_install": true }, "projects": { "~/Desktop/iOS/bajoseekios": { @@ -29,6 +55,7 @@ "trust_level": "trusted" } }, + "export_env_to_zshrc": true, "env": { "DATAEYES_API_KEY": "${codex.key}" } diff --git a/sync/platforms/codex.py b/sync/platforms/codex.py index 854b184..3ed5530 100644 --- a/sync/platforms/codex.py +++ b/sync/platforms/codex.py @@ -183,6 +183,6 @@ def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: print(f"Wrote: {xc_gen}") merge_managed_blocks(xc / "config.toml", shared, generated) - # Export env vars to ~/.zshrc - if cfg.get("env"): + # Export env vars to ~/.zshrc (controlled by export_env_to_zshrc flag) + if cfg.get("export_env_to_zshrc") and cfg.get("env"): _sync_zshrc_env() diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 7156239..73b14bb 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -278,7 +278,7 @@ def toml_section(entries: dict[str, Any], *, ignore: set[str] | None = None) -> Deeper nested dicts become [parent.child] tables. Returns a TOML string suitable for insertion into managed blocks. """ - skip = ignore or {"env", "_comment", "projects", "model_providers"} + skip = ignore or {"env", "_comment", "projects", "model_providers", "export_env_to_zshrc"} lines: list[str] = [] def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: @@ -308,6 +308,9 @@ def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: if key in skip: continue if isinstance(value, dict): + # Insert blank line separator before [table] when following a scalar + if lines and lines[-1] != "": + lines.append("") # Emit as [key] table # Check if any sub-value is itself a dict (deeper nesting) has_deep = any(isinstance(v, dict) for v in value.values()) @@ -334,7 +337,10 @@ def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: for pid, pcfg in providers.items(): if not isinstance(pcfg, dict): continue - lines.append(f"\n[model_providers.{toml_header_key_segment(str(pid))}]") + # Ensure single blank line separator before each provider + if lines and lines[-1] != "": + lines.append("") + lines.append(f"[model_providers.{toml_header_key_segment(str(pid))}]") for k, v in pcfg.items(): lines.append(f"{k} = {toml_value(v)}") @@ -344,7 +350,10 @@ def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: for path, pcfg in projects.items(): if not isinstance(pcfg, dict): continue - lines.append(f"\n[projects.{toml_header_key_segment(str(path))}]") + # Ensure single blank line separator before each project + if lines and lines[-1] != "": + lines.append("") + lines.append(f"[projects.{toml_header_key_segment(str(path))}]") for k, v in pcfg.items(): lines.append(f"{k} = {toml_value(v)}") From 7e2383d2b067feadaa2344da95bd9dad9af73ee9 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 11:37:40 +0800 Subject: [PATCH 05/30] feat(config): expand codex.json with new configuration options - Added options for login shell access, project document size limits, and fallback filenames. - Enhanced sandbox workspace settings with writable roots and environment variable exclusions. - Updated shell environment policy to include ignore default excludes and exclusion lists. - Introduced new features for TUI notifications, animations, and tooltips. - Configured agent settings for maximum threads and depth, along with memory generation and usage options. - Enabled analytics and feedback features for improved user interaction and data collection. --- env/platforms/codex.json | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/env/platforms/codex.json b/env/platforms/codex.json index d2d81a6..d151ac4 100644 --- a/env/platforms/codex.json +++ b/env/platforms/codex.json @@ -9,24 +9,48 @@ "hide_agent_reasoning": true, "sandbox_mode": "workspace-write", "approval_policy": "on-request", + "allow_login_shell": true, "default_permissions": ":workspace", "web_search": "cached", "file_opener": "cursor", + "project_doc_max_bytes": 32768, + "project_doc_fallback_filenames": ["CODEBUDDY.md", "CLAUDE.md"], "history": { "persistence": "save-all", "max_bytes": 104857600 }, "sandbox_workspace_write": { - "network_access": true + "network_access": true, + "writable_roots": [], + "exclude_tmpdir_env_var": false, + "exclude_slash_tmp": false }, "tools": { "view_image": true }, "shell_environment_policy": { - "inherit": "all" + "inherit": "all", + "ignore_default_excludes": false, + "exclude": [] }, "tui": { - "notifications": true + "notifications": true, + "animations": true, + "show_tooltips": true + }, + "agents": { + "max_threads": 6, + "max_depth": 1 + }, + "memories": { + "generate_memories": true, + "use_memories": true + }, + "analytics": { + "enabled": true + }, + "feedback": { + "enabled": true }, "model_providers": { "dataeyes": { @@ -44,6 +68,7 @@ "shell_tool": true, "memories": true, "personality": true, + "fast_mode": true, "enable_request_compression": true, "skill_mcp_dependency_install": true }, From 6dfd3caa2042b4586958df707731ef4e88aac0f2 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 14:39:57 +0800 Subject: [PATCH 06/30] feat(sync): enhance environment variable synchronization for platforms - Updated codex.json to include a structured model_providers section for better configuration management. - Introduced automatic export of environment variables to ~/.zshrc based on the new export_env_to_zshrc flag in sync_config.py. - Refactored codex.py and gemini.py to utilize the new sync_env_to_zshrc function for streamlined environment variable handling. - Removed deprecated zsh synchronization logic from gemini.py, ensuring consistency across platform sync implementations. --- env/platforms/codex.json | 19 +++---- sync/platforms/codex.py | 118 ++++++++++++++++++++------------------- sync/platforms/common.py | 62 ++++++++++++++++++++ sync/platforms/gemini.py | 50 ++--------------- sync/sync_config.py | 24 +++++++- 5 files changed, 158 insertions(+), 115 deletions(-) diff --git a/env/platforms/codex.json b/env/platforms/codex.json index d151ac4..64ece48 100644 --- a/env/platforms/codex.json +++ b/env/platforms/codex.json @@ -1,7 +1,7 @@ { "model": "gpt-5.5", "personality": "pragmatic", - "model_provider": "dataeyes", + "model_provider": "", "model_reasoning_effort": "medium", "model_verbosity": "medium", "model_reasoning_summary": "auto", @@ -15,6 +15,13 @@ "file_opener": "cursor", "project_doc_max_bytes": 32768, "project_doc_fallback_filenames": ["CODEBUDDY.md", "CLAUDE.md"], + "model_providers": { + "dataeyes": { + "base_url": "${codex.url}", + "env_key": "DATAEYES_API_KEY", + "wire_api": "responses" + } + }, "history": { "persistence": "save-all", "max_bytes": 104857600 @@ -52,13 +59,6 @@ "feedback": { "enabled": true }, - "model_providers": { - "dataeyes": { - "base_url": "${codex.url}", - "env_key": "DATAEYES_API_KEY", - "wire_api": "responses" - } - }, "features": { "skills": true, "multi_agent": true, @@ -80,8 +80,7 @@ "trust_level": "trusted" } }, - "export_env_to_zshrc": true, - "env": { + "export_env_to_zshrc": { "DATAEYES_API_KEY": "${codex.key}" } } diff --git a/sync/platforms/codex.py b/sync/platforms/codex.py index 3ed5530..0edbf01 100644 --- a/sync/platforms/codex.py +++ b/sync/platforms/codex.py @@ -1,12 +1,10 @@ import re -import subprocess from pathlib import Path from typing import Any from .common import ( codex_config_path, codex_generated_toml_path, - env_for_platform, load_platform_config, toml_array, toml_header_key_segment, @@ -17,54 +15,6 @@ xcode_codex_dir, ) -ZSHRC_BEGIN = "# BEGIN CODEX ENV SYNC (from env/platforms/codex.json)" -ZSHRC_END = "# END CODEX ENV SYNC" -ZSHRC_BLOCK_PATTERN = re.compile( - r"# BEGIN CODEX ENV SYNC(?: \(from [^)]+\))?" - + r".*?" - + re.escape(ZSHRC_END) - + r"\n?", - re.DOTALL, -) - - -def _generate_zshrc_env_block(cfg: dict[str, Any]) -> str: - """Generate export lines for codex env vars from platform config.""" - env = env_for_platform("codex") - if not env: - return "" - lines = [f'export {k}="{v}"' for k, v in env.items()] - return "\n".join(lines) - - -def _sync_zshrc_env() -> None: - """Write codex env vars into a managed block in ~/.zshrc.""" - body = _generate_zshrc_env_block({}) # cfg not needed, env_for_platform reads from file - if not body: - return - - block = f"{ZSHRC_BEGIN}\n{body}\n{ZSHRC_END}\n" - zshrc = Path.home() / ".zshrc" - - if zshrc.exists(): - text = zshrc.read_text(encoding="utf-8") - if ZSHRC_BLOCK_PATTERN.search(text): - new_text = ZSHRC_BLOCK_PATTERN.sub(block, text) - else: - new_text = text.rstrip() + "\n\n" + block - else: - new_text = block - - zshrc.write_text(new_text, encoding="utf-8") - print(f"Updated codex env vars in {zshrc}.") - - try: - subprocess.run(["zsh", "-c", f"source {zshrc}"], check=True, capture_output=True) - print(f"Sourced {zshrc} (current process).") - except subprocess.CalledProcessError as exc: - print(f"[warn] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") - - MCP_BEGIN = "# BEGIN MCP SYNC (from env/mcp/)" MCP_END = "# END MCP SYNC" MCP_BLOCK_PATTERN = re.compile( @@ -110,16 +60,72 @@ def generate_mcp_toml(servers: dict[str, Any]) -> str: return "\n".join(lines).rstrip() +# Keys that are host-specific — kept in env/platforms/codex.json as reference +# but excluded from managed blocks so each developer can set their own values. +_HOST_SKIP = { + "hide_agent_reasoning", + "web_search", + "file_opener", + "history", + "tools", + "shell_environment_policy", + "tui", + "agents", + "memories", + "analytics", + "feedback", +} + + def generate_shared_toml(cfg: dict[str, Any]) -> str: """Generate Codex platform TOML from platform config. - The platform config follows Codex's official config.toml schema, - so we can use toml_section() for automatic conversion. + Uses toml_section() for automatic conversion of all team-shared settings. + Host-specific keys (_HOST_SKIP) are excluded from the managed blocks — + each developer configures those individually outside the blocks. + + model_provider is handled separately because an empty value should be + emitted as a comment (not as an empty string) so users can easily + uncomment it later. """ lines: list[str] = ["# AUTOGENERATED from env/platforms/codex.json"] - # Top-level keys (model, personality, etc.) - section = toml_section(cfg) + # ── model_provider: commented when empty ── + model_provider = cfg.get("model_provider") + if model_provider: + lines.append(f"model_provider = {toml_quote(str(model_provider))}") + lines.append('preferred_auth_method = "apikey"') + else: + lines.append("# model_provider") + lines.append('# preferred_auth_method = "apikey"') + + # ── model_providers: only output when model_provider is actually set ── + # toml_section's special handler also processes model_providers (outside + # the ignore mechanism), so we remove it from cfg before calling toml_section + # to avoid duplication. + if model_provider: + providers = cfg.get("model_providers") + if isinstance(providers, dict) and providers: + for pid, pcfg in providers.items(): + if not isinstance(pcfg, dict): + continue + lines.extend(["", f"[model_providers.{toml_header_key_segment(str(pid))}]"]) + # Ensure `name` is present (Codex requires it) + if "name" not in pcfg: + lines.append(f"name = {toml_quote(str(pid))}") + for k, v in pcfg.items(): + lines.append(f"{k} = {toml_value(v)}") + + # ── everything else via toml_section ── + # Strip model_providers from cfg so toml_section's special handler won't + # duplicate it. Custom ignore must also include keys that toml_section + # skips by default — a non-empty ignore replaces the default set entirely. + cfg_for_section = {k: v for k, v in cfg.items() if k != "model_providers"} + section = toml_section( + cfg_for_section, + ignore=_HOST_SKIP + | {"model_provider", "model_providers", "env", "export_env_to_zshrc", "projects", "_comment"}, + ) if section.strip(): lines.append(section) @@ -182,7 +188,3 @@ def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: xc_gen.write_text(generated, encoding="utf-8") print(f"Wrote: {xc_gen}") merge_managed_blocks(xc / "config.toml", shared, generated) - - # Export env vars to ~/.zshrc (controlled by export_env_to_zshrc flag) - if cfg.get("export_env_to_zshrc") and cfg.get("env"): - _sync_zshrc_env() diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 73b14bb..729e5b9 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -1,6 +1,7 @@ import json import os import re +import subprocess from pathlib import Path from typing import Any @@ -148,6 +149,67 @@ def env_for_platform(platform: str) -> dict[str, str]: return {k: v for k, v in env.items() if isinstance(k, str) and isinstance(v, str) and v != ""} +def sync_env_to_zshrc(platform: str, env: dict[str, str]) -> None: + """Write platform env vars into a managed block in ~/.zshrc. + + Convention: + - env/platforms/.json declares an "env" object with VAR=value pairs. + - The platform's sync() calls this function (optionally gated by + an "export_env_to_zshrc" flag if the platform supports alternative + env delivery methods like settings.json). + + The managed block format is: + # BEGIN ENV SYNC (from env/platforms/.json) + export VAR="value" + # END ENV SYNC + + Existing blocks for the same platform are replaced in-place; blocks for + other platforms are left untouched. + + The block is sourced in the current subprocess so downstream tools see the + vars, but the user still needs to `source ~/.zshrc` in their open terminal. + """ + if not env: + return + + lines = [f'export {k}="{v}"' for k, v in env.items()] + if not lines: + return + + begin = f"# BEGIN {platform.upper()} ENV SYNC (from env/platforms/{platform}.json)" + end = f"# END {platform.upper()} ENV SYNC" + block = begin + "\n" + "\n".join(lines) + "\n" + end + "\n" + + block_re = re.compile( + r"# BEGIN " + platform.upper() + r" ENV SYNC(?: \(from [^)]+\))?" + + r".*?" + + re.escape(end) + + r"\n?", + re.DOTALL, + ) + + zshrc = Path.home() / ".zshrc" + if zshrc.exists(): + text = zshrc.read_text(encoding="utf-8") + if block_re.search(text): + new_text = block_re.sub(block, text) + else: + new_text = text.rstrip() + "\n\n" + block + else: + new_text = block + + zshrc.write_text(new_text, encoding="utf-8") + print(f"[{platform}] Updated env vars in {zshrc}.") + + # Source so downstream tools see the vars in the current subprocess. + # Does NOT propagate to parent terminal — user must source manually. + try: + subprocess.run(["zsh", "-c", f"source {zshrc}"], check=True, capture_output=True) + print(f"[{platform}] Sourced {zshrc} (current process).") + except subprocess.CalledProcessError as exc: + print(f"[warn] [{platform}] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") + + def filter_mcp_for_platform(mcp_all: dict[str, Any], platform: str) -> dict[str, Any]: """Filter MCP servers to those enabled for the given platform. diff --git a/sync/platforms/gemini.py b/sync/platforms/gemini.py index 668d8b2..cf83dd0 100644 --- a/sync/platforms/gemini.py +++ b/sync/platforms/gemini.py @@ -1,56 +1,14 @@ -import re -import subprocess from pathlib import Path from typing import Any -from .common import sync_json_mcp +from .common import sync_env_to_zshrc, sync_json_mcp _TARGET = Path.home() / ".gemini/settings.json" -ZSHRC_BEGIN = "# BEGIN GEMINI ENV SYNC (from env/platforms/gemini.json)" -ZSHRC_END = "# END GEMINI ENV SYNC" -ZSHRC_BLOCK_PATTERN = re.compile( - r"# BEGIN GEMINI ENV SYNC(?: \(from [^)]+\))?" - + r".*?" - + re.escape(ZSHRC_END) - + r"\n?", - re.DOTALL, -) - - -def _sync_zshrc_env(cfg: dict[str, Any]) -> None: - env = cfg.get("env", {}) - if not isinstance(env, dict) or not env: - return - - lines = [f'export {k}="{v}"' for k, v in env.items() if isinstance(k, str) and isinstance(v, str) and v] - if not lines: - return - - block = f"{ZSHRC_BEGIN}\n" + "\n".join(lines) + f"\n{ZSHRC_END}\n" - zshrc = Path.home() / ".zshrc" - - if zshrc.exists(): - text = zshrc.read_text(encoding="utf-8") - if ZSHRC_BLOCK_PATTERN.search(text): - new_text = ZSHRC_BLOCK_PATTERN.sub(block, text) - else: - new_text = text.rstrip() + "\n\n" + block - else: - new_text = block - - zshrc.write_text(new_text, encoding="utf-8") - print(f"Updated gemini env vars in {zshrc}.") - - try: - subprocess.run(["zsh", "-c", f"source {zshrc}"], check=True, capture_output=True) - print(f"Sourced {zshrc} (current process).") - except subprocess.CalledProcessError as exc: - print(f"[warn] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") - def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: """Sync MCP servers and env vars to Gemini CLI.""" sync_json_mcp(_TARGET, mcp_servers) - if cfg.get("env"): - _sync_zshrc_env(cfg) + env = cfg.get("env", {}) + if isinstance(env, dict) and env: + sync_env_to_zshrc("gemini", env) diff --git a/sync/sync_config.py b/sync/sync_config.py index d6a38a5..f12084b 100644 --- a/sync/sync_config.py +++ b/sync/sync_config.py @@ -15,7 +15,7 @@ from typing import Any from platforms import claude, cline, codebuddy, codex, cursor, gemini -from platforms.common import discover_platforms, filter_mcp_for_platform, load_all_mcp, load_platform_config +from platforms.common import discover_platforms, filter_mcp_for_platform, load_all_mcp, load_platform_config, sync_env_to_zshrc # continue.py contains 'continue' keyword which can't be a Python import name. import importlib as _importlib @@ -72,6 +72,26 @@ def _s(mcp_servers: dict[str, Any], _platform_cfg: dict[str, Any]) -> None: return all_targets +def _auto_export_env_to_zshrc(platform: str, platform_cfg: dict[str, Any]) -> None: + """Automatically write env vars to ~/.zshrc if platform config declares + an export_env_to_zshrc block. + + Convention: env/platforms/.json may contain: + + "export_env_to_zshrc": { + "VAR_NAME": "value" + } + + Each key in the object is treated as an env var to export. + When present, the orchestrator calls sync_env_to_zshrc() so that each + platform's sync() doesn't need to handle zshrc manually. + """ + env = platform_cfg.get("export_env_to_zshrc") + if not isinstance(env, dict) or not env: + return + sync_env_to_zshrc(platform, env) + + def main() -> None: mcp_all = load_all_mcp() if not mcp_all: @@ -99,11 +119,13 @@ def main() -> None: mcp_servers = filter_mcp_for_platform(mcp_all, name) platform_cfg = load_platform_config(name) fn(mcp_servers, platform_cfg) + _auto_export_env_to_zshrc(name, platform_cfg) elif args.target in all_targets: fn = all_targets[args.target] mcp_servers = filter_mcp_for_platform(mcp_all, args.target) platform_cfg = load_platform_config(args.target) fn(mcp_servers, platform_cfg) + _auto_export_env_to_zshrc(args.target, platform_cfg) else: print(f"[error] Unknown target '{args.target}'. Valid: all, {', '.join(valid)}", file=sys.stderr) raise SystemExit(1) From 86d441e10932e6cc75a7787855615e1e172be6de Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 15:34:17 +0800 Subject: [PATCH 07/30] refactor(sync): simplify environment variable handling and improve code clarity - Removed direct sourcing of ~/.zshrc in sync_all.sh, replacing it with a user instruction to source manually if environment variables are updated. - Refactored codex.py to output model_providers after root settings, ensuring proper configuration management. - Updated common.py to enforce valid environment variable names and improved export formatting for consistency. --- sync/platforms/codex.py | 30 ++--- sync/platforms/common.py | 27 ++-- sync/sync_all.sh | 7 +- tests/test_codex_sync.py | 264 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+), 38 deletions(-) create mode 100644 tests/test_codex_sync.py diff --git a/sync/platforms/codex.py b/sync/platforms/codex.py index 0edbf01..2d7ba1e 100644 --- a/sync/platforms/codex.py +++ b/sync/platforms/codex.py @@ -99,23 +99,6 @@ def generate_shared_toml(cfg: dict[str, Any]) -> str: lines.append("# model_provider") lines.append('# preferred_auth_method = "apikey"') - # ── model_providers: only output when model_provider is actually set ── - # toml_section's special handler also processes model_providers (outside - # the ignore mechanism), so we remove it from cfg before calling toml_section - # to avoid duplication. - if model_provider: - providers = cfg.get("model_providers") - if isinstance(providers, dict) and providers: - for pid, pcfg in providers.items(): - if not isinstance(pcfg, dict): - continue - lines.extend(["", f"[model_providers.{toml_header_key_segment(str(pid))}]"]) - # Ensure `name` is present (Codex requires it) - if "name" not in pcfg: - lines.append(f"name = {toml_quote(str(pid))}") - for k, v in pcfg.items(): - lines.append(f"{k} = {toml_value(v)}") - # ── everything else via toml_section ── # Strip model_providers from cfg so toml_section's special handler won't # duplicate it. Custom ignore must also include keys that toml_section @@ -129,6 +112,19 @@ def generate_shared_toml(cfg: dict[str, Any]) -> str: if section.strip(): lines.append(section) + # ── model_providers: output after root settings so later keys stay at root ── + providers = cfg.get("model_providers") + if isinstance(providers, dict) and providers: + for pid, pcfg in providers.items(): + if not isinstance(pcfg, dict): + continue + lines.extend(["", f"[model_providers.{toml_header_key_segment(str(pid))}]"]) + # Ensure `name` is present (Codex requires it) + if "name" not in pcfg: + lines.append(f"name = {toml_quote(str(pid))}") + for k, v in pcfg.items(): + lines.append(f"{k} = {toml_value(v)}") + return "\n".join(lines).rstrip() diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 729e5b9..aaf64fd 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -1,7 +1,7 @@ import json import os import re -import subprocess +import shlex from pathlib import Path from typing import Any @@ -11,6 +11,7 @@ SECRETS_PATH = REPO_ROOT / "env" / "secrets.json" _SECRET_REF_RE = re.compile(r'\$\{([^}]+)\}') +_ENV_VAR_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z") # ── Secrets resolution ─────────────────────────────────────────────────────── @@ -160,19 +161,22 @@ def sync_env_to_zshrc(platform: str, env: dict[str, str]) -> None: The managed block format is: # BEGIN ENV SYNC (from env/platforms/.json) - export VAR="value" + export VAR='value' # END ENV SYNC Existing blocks for the same platform are replaced in-place; blocks for other platforms are left untouched. - The block is sourced in the current subprocess so downstream tools see the - vars, but the user still needs to `source ~/.zshrc` in their open terminal. + The user still needs to `source ~/.zshrc` in their open terminal. """ if not env: return - lines = [f'export {k}="{v}"' for k, v in env.items()] + lines: list[str] = [] + for k, v in env.items(): + if not _ENV_VAR_NAME_RE.fullmatch(k): + raise ValueError(f"Invalid environment variable name for {platform}: {k!r}") + lines.append(f"export {k}={shlex.quote(str(v))}") if not lines: return @@ -200,14 +204,7 @@ def sync_env_to_zshrc(platform: str, env: dict[str, str]) -> None: zshrc.write_text(new_text, encoding="utf-8") print(f"[{platform}] Updated env vars in {zshrc}.") - - # Source so downstream tools see the vars in the current subprocess. - # Does NOT propagate to parent terminal — user must source manually. - try: - subprocess.run(["zsh", "-c", f"source {zshrc}"], check=True, capture_output=True) - print(f"[{platform}] Sourced {zshrc} (current process).") - except subprocess.CalledProcessError as exc: - print(f"[warn] [{platform}] source {zshrc} exited {exc.returncode}: {exc.stderr.decode().strip()}") + print(f"[{platform}] Run 'source {zshrc}' in your terminal to apply changes.") def filter_mcp_for_platform(mcp_all: dict[str, Any], platform: str) -> dict[str, Any]: @@ -395,7 +392,7 @@ def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: # model_providers section providers = entries.get("model_providers") - if isinstance(providers, dict): + if "model_providers" not in skip and isinstance(providers, dict): for pid, pcfg in providers.items(): if not isinstance(pcfg, dict): continue @@ -408,7 +405,7 @@ def _emit_table(parent_key: str, sub: dict[str, Any]) -> None: # projects section projects = entries.get("projects") - if isinstance(projects, dict): + if "projects" not in skip and isinstance(projects, dict): for path, pcfg in projects.items(): if not isinstance(pcfg, dict): continue diff --git a/sync/sync_all.sh b/sync/sync_all.sh index 43de090..7c985ab 100755 --- a/sync/sync_all.sh +++ b/sync/sync_all.sh @@ -34,11 +34,6 @@ bash "$SCRIPT_DIR/backup-config.sh" backup echo "[1/1] Sync config to Cursor / CodeBuddy / Codex / Claude / Cline / Xcode" python3 "$SCRIPT_DIR/sync_config.py" --target all -# Source ~/.zshrc to load any env vars written by the sync. -if [ -f "$HOME/.zshrc" ]; then - # shellcheck disable=SC1090 - source "$HOME/.zshrc" 2>/dev/null || true - echo "[sync] Sourced ~/.zshrc — run 'source ~/.zshrc' in your terminal for immediate effect." -fi +echo "[sync] If env vars were updated, run 'source ~/.zshrc' in your terminal to apply them." echo "Done." diff --git a/tests/test_codex_sync.py b/tests/test_codex_sync.py new file mode 100644 index 0000000..fe5829b --- /dev/null +++ b/tests/test_codex_sync.py @@ -0,0 +1,264 @@ +import contextlib +import io +import json +import os +import sys +import tempfile +import tomllib +import unittest +from collections.abc import Mapping +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SYNC_DIR = REPO_ROOT / "sync" +if str(SYNC_DIR) not in sys.path: + sys.path.insert(0, str(SYNC_DIR)) + +import sync_config # noqa: E402 +from platforms import common # noqa: E402 + + +@contextlib.contextmanager +def patched_sync_environment(root: Path): + old_env = {k: os.environ.get(k) for k in ("HOME", "CODEX_HOME", "CODEX_CONFIG")} + old_paths = (common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH) + old_argv = sys.argv[:] + try: + os.environ["HOME"] = str(root / "home") + os.environ["CODEX_HOME"] = str(root / "home" / ".codex") + os.environ.pop("CODEX_CONFIG", None) + common.MCP_DIR = root / "env" / "mcp" + common.PLATFORMS_DIR = root / "env" / "platforms" + common.SECRETS_PATH = root / "env" / "secrets.json" + yield + finally: + for key, value in old_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH = old_paths + sys.argv = old_argv + + +class CodexSyncTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + self.platform_cfg = json.loads((REPO_ROOT / "env" / "platforms" / "codex.json").read_text()) + self._write_json( + self.root / "env" / "mcp" / "sample.json", + { + "name": "sample", + "type": "stdio", + "command": "echo", + "args": ["hello"], + "platforms": ["codex"], + }, + ) + self._write_json( + self.root / "env" / "secrets.json", + {"codex": {"url": "https://codex.example/v1", "key": "sk-test-value"}}, + ) + + def tearDown(self) -> None: + self.tmp.cleanup() + + def _write_json(self, path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8") + + def _run_codex_sync(self, cfg: dict) -> tuple[str, dict]: + self._write_json(self.root / "env" / "platforms" / "codex.json", cfg) + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "codex"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + config_text = (self.root / "home" / ".codex" / "config.toml").read_text(encoding="utf-8") + return config_text, tomllib.loads(config_text) + + def assert_nested_equal(self, data: Mapping, expected: Mapping, path: str) -> None: + for key, expected_value in expected.items(): + current_path = f"{path}.{key}" + self.assertIn(key, data, current_path) + actual_value = data[key] + if isinstance(expected_value, Mapping): + self.assertIsInstance(actual_value, Mapping, current_path) + self.assert_nested_equal(actual_value, expected_value, current_path) + else: + self.assertEqual(actual_value, expected_value, current_path) + + def test_model_provider_value_syncs_selector_and_provider_table(self) -> None: + cfg = dict(self.platform_cfg) + cfg["model_provider"] = "dataeyes" + + config_text, parsed = self._run_codex_sync(cfg) + + self.assertIn('model_provider = "dataeyes"', config_text) + self.assertIn('preferred_auth_method = "apikey"', config_text) + self.assertEqual(parsed["model_provider"], "dataeyes") + self.assertEqual(parsed["model_providers"]["dataeyes"]["name"], "dataeyes") + self.assertEqual(parsed["model_providers"]["dataeyes"]["base_url"], "https://codex.example/v1") + + def test_model_provider_null_syncs_commented_selector_and_provider_table(self) -> None: + cfg = dict(self.platform_cfg) + cfg["model_provider"] = None + + config_text, parsed = self._run_codex_sync(cfg) + + self.assertIn("# model_provider", config_text) + self.assertNotIn("model_provider", parsed) + self.assertEqual(parsed["model_providers"]["dataeyes"]["name"], "dataeyes") + self.assertEqual(parsed["model_providers"]["dataeyes"]["base_url"], "https://codex.example/v1") + + def test_model_provider_empty_syncs_commented_selector_and_provider_table(self) -> None: + cfg = dict(self.platform_cfg) + cfg["model_provider"] = "" + + config_text, parsed = self._run_codex_sync(cfg) + + self.assertIn("# model_provider", config_text) + self.assertNotIn("model_provider", parsed) + self.assertEqual(parsed["model_providers"]["dataeyes"]["name"], "dataeyes") + self.assertEqual(parsed["model_providers"]["dataeyes"]["base_url"], "https://codex.example/v1") + + def test_export_env_to_zshrc_replaces_missing_dataeyes_key_with_managed_block(self) -> None: + cfg = dict(self.platform_cfg) + zshrc = self.root / "home" / ".zshrc" + zshrc.parent.mkdir(parents=True, exist_ok=True) + zshrc.write_text("export OTHER=value\n", encoding="utf-8") + + self._run_codex_sync(cfg) + + zshrc_text = zshrc.read_text(encoding="utf-8") + self.assertEqual(zshrc_text.count("DATAEYES_API_KEY"), 1) + self.assertIn("# BEGIN CODEX ENV SYNC (from env/platforms/codex.json)\n", zshrc_text) + self.assertIn("export DATAEYES_API_KEY=sk-test-value\n", zshrc_text) + self.assertTrue(zshrc_text.endswith("# END CODEX ENV SYNC\n")) + + def test_export_env_to_zshrc_replaces_existing_managed_dataeyes_key(self) -> None: + cfg = dict(self.platform_cfg) + zshrc = self.root / "home" / ".zshrc" + zshrc.parent.mkdir(parents=True, exist_ok=True) + zshrc.write_text( + "before\n" + "# BEGIN CODEX ENV SYNC (from env/platforms/codex.json)\n" + "export DATAEYES_API_KEY=old-value\n" + "# END CODEX ENV SYNC\n" + "after\n", + encoding="utf-8", + ) + + self._run_codex_sync(cfg) + + zshrc_text = zshrc.read_text(encoding="utf-8") + self.assertNotIn("old-value", zshrc_text) + self.assertEqual(zshrc_text.count("DATAEYES_API_KEY"), 1) + self.assertIn("export DATAEYES_API_KEY=sk-test-value\n", zshrc_text) + self.assertTrue(zshrc_text.startswith("before\n")) + self.assertTrue(zshrc_text.endswith("after\n")) + + def test_codex_json_properties_are_mapped_or_excluded_as_expected(self) -> None: + config_text, parsed = self._run_codex_sync(self.platform_cfg) + + covered_keys = { + "model", + "personality", + "model_provider", + "model_reasoning_effort", + "model_verbosity", + "model_reasoning_summary", + "plan_mode_reasoning_effort", + "hide_agent_reasoning", + "sandbox_mode", + "approval_policy", + "allow_login_shell", + "default_permissions", + "web_search", + "file_opener", + "project_doc_max_bytes", + "project_doc_fallback_filenames", + "model_providers", + "history", + "sandbox_workspace_write", + "tools", + "shell_environment_policy", + "tui", + "agents", + "memories", + "analytics", + "feedback", + "features", + "projects", + "export_env_to_zshrc", + } + self.assertEqual(set(self.platform_cfg), covered_keys) + + expected_root = { + "model": "gpt-5.5", + "personality": "pragmatic", + "model_reasoning_effort": "medium", + "model_verbosity": "medium", + "model_reasoning_summary": "auto", + "plan_mode_reasoning_effort": "medium", + "sandbox_mode": "workspace-write", + "approval_policy": "on-request", + "allow_login_shell": True, + "default_permissions": ":workspace", + "project_doc_max_bytes": 32768, + "project_doc_fallback_filenames": ["CODEBUDDY.md", "CLAUDE.md"], + } + for key, expected in expected_root.items(): + self.assertEqual(parsed[key], expected, key) + + self.assert_nested_equal( + parsed, + { + "sandbox_workspace_write": { + "network_access": True, + "writable_roots": [], + "exclude_tmpdir_env_var": False, + "exclude_slash_tmp": False, + }, + "features": { + "skills": True, + "multi_agent": True, + "hooks": True, + "shell_snapshot": True, + "unified_exec": True, + "shell_tool": True, + "memories": True, + "personality": True, + "fast_mode": True, + "enable_request_compression": True, + "skill_mcp_dependency_install": True, + }, + }, + "parsed", + ) + self.assertEqual(parsed["model_providers"]["dataeyes"]["base_url"], "https://codex.example/v1") + self.assertEqual(parsed["model_providers"]["dataeyes"]["env_key"], "DATAEYES_API_KEY") + self.assertEqual(parsed["model_providers"]["dataeyes"]["wire_api"], "responses") + + for host_specific_key in ( + "hide_agent_reasoning", + "web_search", + "file_opener", + "history", + "tools", + "shell_environment_policy", + "tui", + "agents", + "memories", + "analytics", + "feedback", + "projects", + "export_env_to_zshrc", + ): + self.assertNotIn(host_specific_key, parsed, host_specific_key) + self.assertNotIn(f"[{host_specific_key}]", config_text) + + +if __name__ == "__main__": + unittest.main() From d1b3927150e418a5e03e5506015e88db90aad597 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 15:40:50 +0800 Subject: [PATCH 08/30] docs(README): add platform support section for macOS and Windows users - Introduced a new section in README.md detailing platform support, highlighting that the primary development and testing environment is macOS. - Noted that certain features may not be available outside of macOS and encouraged Windows users to validate and contribute through PRs. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c2909bd..3577945 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ $EDITOR env/secrets.json bash sync.sh ``` +## 平台支持 + +当前主要开发和测试环境为 **macOS**。部分平台模块(如 Codex 同步中的 Xcode 集成、`.zshrc` 导出)在 macOS 外不可用。 + +欢迎 Windows 用户在 Windows 上验证并提交 PR。核心同步逻辑已尽量保持跨平台,适配改动预计较小。 + ## 模块 各模块有独立的 README,按需深入: From 39dee36959a221180f16ce90d4e4c05226e8306e Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 15:54:09 +0800 Subject: [PATCH 09/30] feat(config): update Claude configuration and sync logic - Expanded `claude.json` with detailed comments and new settings for model behavior, permissions, and host-specific configurations. - Refactored `claude.py` to improve path handling and added a function to generate managed settings, excluding host-specific keys. - Simplified the hook installation process and enhanced the synchronization logic for Xcode Claude Agent configuration. - Updated `xmcp-init.sh` to a basic script for testing purposes, replacing the previous complex logic. --- env/platforms/claude.json | 46 ++++ hooks/xmcp-init.sh | 73 +----- sync/platforms/claude.py | 192 ++++++++++++--- tests/test_claude_sync.py | 487 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 699 insertions(+), 99 deletions(-) create mode 100644 tests/test_claude_sync.py diff --git a/env/platforms/claude.json b/env/platforms/claude.json index a9f5646..755febe 100644 --- a/env/platforms/claude.json +++ b/env/platforms/claude.json @@ -1,4 +1,15 @@ { + "_comment": "Claude Code team-shared configuration. Host-specific keys are listed in _hostSettings for reference but excluded during sync — each developer configures them individually in ~/.claude/settings.json.", + "model": "claude-sonnet-4-6", + "effortLevel": "medium", + "alwaysThinkingEnabled": true, + "outputStyle": "Explanatory", + "includeGitInstructions": true, + "respectGitignore": true, + "fileCheckpointingEnabled": true, + "autoCompactEnabled": true, + "autoMemoryEnabled": true, + "respondToBashCommands": true, "env": { "ANTHROPIC_AUTH_TOKEN": "${claude.token}", "ANTHROPIC_BASE_URL": "${claude.url}", @@ -8,6 +19,19 @@ "CLAUDE_CODE_EFFORT_LEVEL": "medium", "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1" }, + "permissions": { + "allow": [ + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git status *)", + "Bash(git branch *)" + ], + "deny": [ + "Bash(curl *)", + "Bash(wget *)" + ], + "defaultMode": "default" + }, "hooks": { "SessionStart": [ { @@ -20,5 +44,27 @@ ] } ] + }, + "_hostSettings": { + "_comment": "Host-specific settings — listed for reference only. Excluded from managed sync. Each developer configures these in ~/.claude/settings.json.", + "theme": "dark", + "tui": "fullscreen", + "editorMode": "normal", + "preferredNotifChannel": "auto", + "viewMode": "default", + "showTurnDuration": true, + "showThinkingSummaries": false, + "autoScrollEnabled": true, + "spinnerTipsEnabled": true, + "syntaxHighlightingDisabled": false, + "terminalProgressBarEnabled": true, + "wheelScrollAccelerationEnabled": true, + "prefersReducedMotion": false, + "axScreenReaderRender": false, + "cleanupPeriodDays": 30, + "defaultShell": "bash", + "autoUpdatesChannel": "stable", + "feedbackSurveyRate": 0.05, + "language": "chinese" } } diff --git a/hooks/xmcp-init.sh b/hooks/xmcp-init.sh index b8abad6..8e352c1 100755 --- a/hooks/xmcp-init.sh +++ b/hooks/xmcp-init.sh @@ -1,71 +1,2 @@ -#!/usr/bin/env bash -# Auto-create .xcodebuildmcp/config.yaml if this is an iOS/macOS Xcode project - -[ -f .xcodebuildmcp/config.yaml ] && exit 0 - -ws=$(find . -maxdepth 2 -name "*.xcworkspace" ! -path "*/Pods/*" 2>/dev/null | head -1) -[ -z "$ws" ] && exit 0 - -ws_name=$(basename "$ws") -scheme="${ws_name%.xcworkspace}-Debug" - -# Auto-discover first available iPhone simulator (prefer iPhone 16, fall back to newest available) -sim_info=$(xcrun simctl list devices available iPhone 2>/dev/null | grep -v "unavailable" | grep "iPhone 16" | head -1) -if [ -z "$sim_info" ]; then - sim_info=$(xcrun simctl list devices available iPhone 2>/dev/null | grep -v "unavailable" | head -1) -fi - -sim_name="iPhone 16" -sim_udid="" -if [ -n "$sim_info" ]; then - sim_name=$(echo "$sim_info" | sed 's/ (.*//' | xargs) - sim_udid=$(echo "$sim_info" | grep -oE '[A-F0-9]{8}-([A-F0-9]{4}-){3}[A-F0-9]{12}' | head -1) -fi - -mkdir -p .xcodebuildmcp -if [ -n "$sim_udid" ]; then -cat > .xcodebuildmcp/config.yaml << EOF -schemaVersion: 1 - -sessionDefaults: - workspace: "${ws_name}" - scheme: "${scheme}" - configuration: "Debug" - simulatorName: "${sim_name}" - simulatorUdid: "${sim_udid}" - simulatorPlatform: "iOS Simulator" - -incrementalBuildsEnabled: true -parallelTestingEnabled: true -dapRequestTimeoutMs: 60000 - -enabledWorkflows: - - "simulator" - - "ui-automation" - -filePathRenderStyle: "tree" -showTestTiming: true -EOF -else -cat > .xcodebuildmcp/config.yaml << EOF -schemaVersion: 1 - -sessionDefaults: - workspace: "${ws_name}" - scheme: "${scheme}" - configuration: "Debug" - simulatorName: "${sim_name}" - simulatorPlatform: "iOS Simulator" - -incrementalBuildsEnabled: true -parallelTestingEnabled: true -dapRequestTimeoutMs: 60000 - -enabledWorkflows: - - "simulator" - - "ui-automation" - -filePathRenderStyle: "tree" -showTestTiming: true -EOF -fi +#!/bin/bash +echo 'hello' diff --git a/sync/platforms/claude.py b/sync/platforms/claude.py index a320808..b2b8d0c 100644 --- a/sync/platforms/claude.py +++ b/sync/platforms/claude.py @@ -1,33 +1,124 @@ +import copy from pathlib import Path from typing import Any from .common import merge_object, read_json_object, write_json -CLAUDE_JSON = Path.home() / ".claude.json" -CLAUDE_SETTINGS_JSON = Path.home() / ".claude" / "settings.json" -CLAUDE_HOOKS_DIR = Path.home() / ".claude" / "hooks" -XCODE_CLAUDE_JSON = Path.home() / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json" -REPO_HOOKS_DIR = Path(__file__).resolve().parents[2] / "hooks" +# ── Path helpers (functions so they respect HOME env var at runtime) ── + + +def claude_json_path() -> Path: + return Path.home() / ".claude.json" + + +def claude_settings_json_path() -> Path: + return Path.home() / ".claude" / "settings.json" + + +def claude_settings_generated_json_path() -> Path: + return Path.home() / ".claude" / "settings.generated.json" + + +def claude_hooks_dir_path() -> Path: + return Path.home() / ".claude" / "hooks" + + +def xcode_claude_json_path() -> Path: + return Path.home() / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json" + + +def _repo_hooks_dir() -> Path: + return Path(__file__).resolve().parents[2] / "hooks" + +# ── Host-specific keys ── +# These keys are kept in env/platforms/claude.json as reference but excluded +# from managed (team-shared) settings — each developer sets them individually. +_HOST_SKIP = { + # Personal UI/UX preferences + "apiKeyHelper", + "theme", + "tui", + "editorMode", + "preferredNotifChannel", + "statusLine", + "voice", + "voiceEnabled", + "viewMode", + "prefersReducedMotion", + "syntaxHighlightingDisabled", + "terminalProgressBarEnabled", + "wheelScrollAccelerationEnabled", + "axScreenReaderRender", + "showTurnDuration", + "showThinkingSummaries", + "showClearContextOnPlanAccept", + "autoScrollEnabled", + "spinnerTipsEnabled", + "spinnerTipsOverride", + "spinnerVerbs", + "companyAnnouncements", + "footerLinksRegexes", + "language", + "ultracode", + "fastModePerSessionOptIn", + # Host-specific tooling & paths + "autoConnectIde", + "autoInstallIdeExtension", + "externalEditorContext", + "fileSuggestion", + "feedbackSurveyRate", + "cleanupPeriodDays", + "defaultShell", + "prUrlTemplate", + "autoUpdatesChannel", + "sshConfigs", + "worktree", + "plansDirectory", + "autoMemoryDirectory", + # Agent / teammate preferences + "teammateMode", + "teammateDefaultModel", + "disableAgentView", + "agent", + "agentPushNotifEnabled", + "inputNeededNotifEnabled", + "remoteControlAtStartup", + # Cloud auth helpers (host-specific) + "awsAuthRefresh", + "awsCredentialExport", + "gcpAuthRefresh", + "otelHeadersHelper", + # Managed-only keys (only in managed-settings.json, not user settings) + "claudeMd", + "claudeMdExcludes", + "policyHelper", + # Misc personal + "skipWebFetchPreflight", +} def _install_hook_scripts() -> None: - if not REPO_HOOKS_DIR.exists(): + """Install hook shell scripts from repo hooks/ into ~/.claude/hooks/.""" + repo_dir = _repo_hooks_dir() + if not repo_dir.exists(): return - CLAUDE_HOOKS_DIR.mkdir(parents=True, exist_ok=True) - for script in sorted(REPO_HOOKS_DIR.glob("*.sh")): - dest = CLAUDE_HOOKS_DIR / script.name + hooks_dir = claude_hooks_dir_path() + hooks_dir.mkdir(parents=True, exist_ok=True) + for script in sorted(repo_dir.glob("*.sh")): + dest = hooks_dir / script.name dest.write_text(script.read_text(encoding="utf-8"), encoding="utf-8") dest.chmod(0o755) print(f"Installed hook script: {dest}") def _expand_hooks(cfg: dict[str, Any]) -> dict[str, Any]: + """Expand ~ paths in hook command entries from platform config.""" raw: dict[str, Any] = cfg.get("hooks", {}) expanded: dict[str, Any] = {} for event, entries in raw.items(): - expanded_entries = [] + expanded_entries: list[dict[str, Any]] = [] for entry in entries: - hooks_list = [] + hooks_list: list[dict[str, Any]] = [] for hook in entry.get("hooks", []): h = dict(hook) if "command" in h: @@ -38,8 +129,32 @@ def _expand_hooks(cfg: dict[str, Any]) -> dict[str, Any]: return expanded +def generate_managed_settings(cfg: dict[str, Any]) -> dict[str, Any]: + """Generate managed (team-shared) settings dict from platform config. + + Excludes host-specific keys (_HOST_SKIP) and keys handled separately + (env, hooks, internal keys) so each developer can set those individually. + """ + managed: dict[str, Any] = {} + for key, value in cfg.items(): + if key in _HOST_SKIP: + continue + if key.startswith("_"): + continue # Internal / reference keys (_comment, _hostSettings, etc.) + if key == "env": + continue # Merged separately into settings.json + if key == "hooks": + continue # Handled via _expand_hooks + merge + if key == "export_env_to_zshrc": + continue # Handled by orchestrator + managed[key] = copy.deepcopy(value) + return managed + + def _sync_xcode_claude_json(servers: dict[str, Any]) -> None: - data = read_json_object(XCODE_CLAUDE_JSON) + """Sync MCP servers into Xcode Claude Agent config.""" + path = xcode_claude_json_path() + data = read_json_object(path) projects = data.get("projects") if isinstance(projects, dict) and projects: for proj in projects.values(): @@ -49,40 +164,61 @@ def _sync_xcode_claude_json(servers: dict[str, Any]) -> None: else: data["mcpServers"] = servers mode = "root" - write_json(XCODE_CLAUDE_JSON, data) - print(f"Replaced MCP servers in {XCODE_CLAUDE_JSON} ({mode}).") + write_json(path, data) + print(f"Replaced MCP servers in {path} ({mode}).") def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: - """Sync MCP servers and Claude platform config.""" - # Write ~/.claude.json - claude = read_json_object(CLAUDE_JSON) + """Sync MCP servers and Claude Code platform config. + + Steps: + 1. Write ~/.claude.json with MCP servers (preserving other top-level keys). + 2. Sync Xcode Claude Agent config. + 3. Generate ~/.claude/settings.generated.json for team-shared settings. + 4. Merge env and hooks into ~/.claude/settings.json. + 5. Install hook shell scripts. + """ + # ── 1. ~/.claude.json — MCP servers ── + cj_path = claude_json_path() + claude = read_json_object(cj_path) claude["mcpServers"] = mcp_servers - write_json(CLAUDE_JSON, claude) - print(f"Replaced MCP servers in {CLAUDE_JSON} (other top-level config preserved).") + write_json(cj_path, claude) + print(f"Replaced MCP servers in {cj_path} (other top-level config preserved).") - # Xcode Claude Agent + # ── 2. Xcode Claude Agent ── _sync_xcode_claude_json(mcp_servers) - # Write ~/.claude/settings.json - settings = read_json_object(CLAUDE_SETTINGS_JSON) + # ── 3. settings.generated.json — team-shared settings ── + managed = generate_managed_settings(cfg) + gen_path = claude_settings_generated_json_path() + gen_path.parent.mkdir(parents=True, exist_ok=True) + write_json(gen_path, managed) + if managed: + print(f"Wrote {gen_path} ({len(managed)} team-shared keys).") + else: + print(f"[claude] No team-shared settings to generate — {gen_path} unchanged.") + + # ── 4. settings.json — merge env and hooks into user settings ── + settings_path = claude_settings_json_path() + settings = read_json_object(settings_path) + # 4a. Merge env env = cfg.get("env", {}) if isinstance(env, dict) and env: settings["env"] = merge_object(settings.get("env"), env) - print(f"Merged env into {CLAUDE_SETTINGS_JSON} ({len(env)} vars; other keys preserved).") + print(f"Merged env into {settings_path} ({len(env)} vars; other keys preserved).") else: - print(f"[claude] No env vars in platform config — skipping env merge.") + print("[claude] No env vars in platform config — skipping env merge.") - # Install hook scripts + # 4b. Install hook scripts _install_hook_scripts() - # Merge hooks from platform config + # 4c. Merge hooks config_hooks = _expand_hooks(cfg) if config_hooks: existing_hooks: dict[str, Any] = settings.get("hooks", {}) existing_hooks.update(config_hooks) settings["hooks"] = existing_hooks - print(f"Merged hooks into {CLAUDE_SETTINGS_JSON} ({len(config_hooks)} event(s)).") + print(f"Merged hooks into {settings_path} ({len(config_hooks)} event(s)).") - write_json(CLAUDE_SETTINGS_JSON, settings) + write_json(settings_path, settings) diff --git a/tests/test_claude_sync.py b/tests/test_claude_sync.py new file mode 100644 index 0000000..9561092 --- /dev/null +++ b/tests/test_claude_sync.py @@ -0,0 +1,487 @@ +import contextlib +import io +import json +import os +import stat +import sys +import tempfile +import unittest +from collections.abc import Mapping +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SYNC_DIR = REPO_ROOT / "sync" +if str(SYNC_DIR) not in sys.path: + sys.path.insert(0, str(SYNC_DIR)) + +import sync_config # noqa: E402 +from platforms import common # noqa: E402 +from platforms import claude as claude_module # noqa: E402 + + +@contextlib.contextmanager +def patched_sync_environment(root: Path): + """Redirect HOME and common module paths to an isolated test root.""" + old_env = {k: os.environ.get(k) for k in ("HOME",)} + old_paths = (common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH) + old_argv = sys.argv[:] + try: + os.environ["HOME"] = str(root / "home") + common.MCP_DIR = root / "env" / "mcp" + common.PLATFORMS_DIR = root / "env" / "platforms" + common.SECRETS_PATH = root / "env" / "secrets.json" + yield + finally: + for key, value in old_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH = old_paths + sys.argv = old_argv + + +def _run_claude_sync(root: Path, cfg: dict) -> None: + """Run sync_config --target claude within the patched environment.""" + # Write platform config and required secrets + (root / "env" / "platforms").mkdir(parents=True, exist_ok=True) + (root / "env" / "platforms" / "claude.json").write_text( + json.dumps(cfg, indent=4) + "\n", encoding="utf-8" + ) + + with patched_sync_environment(root): + sys.argv = ["sync_config.py", "--target", "claude"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + + +class ClaudeSyncTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + self.home = self.root / "home" + self.platform_cfg = json.loads( + (REPO_ROOT / "env" / "platforms" / "claude.json").read_text() + ) + + # Seed env/mcp/ with a sample server + self._write_json( + self.root / "env" / "mcp" / "sample.json", + { + "name": "sample", + "type": "stdio", + "command": "echo", + "args": ["hello"], + "platforms": ["claude"], + }, + ) + # Seed secrets for env var resolution + self._write_json( + self.root / "env" / "secrets.json", + { + "claude": { + "token": "sk-ant-test-token", + "url": "https://claude.example/v1", + } + }, + ) + + # Ensure repo hooks/ directory exists for hook script install tests + self._repo_hooks_dir = REPO_ROOT / "hooks" + if not self._repo_hooks_dir.exists(): + self._repo_hooks_dir.mkdir(parents=True, exist_ok=True) + # Ensure at least one hook script exists + (self._repo_hooks_dir / "xmcp-init.sh").touch() + + def tearDown(self) -> None: + self.tmp.cleanup() + + # ── helpers ──────────────────────────────────────────────────────────────── + + def _write_json(self, path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8") + + def _read_json(self, path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + def assert_nested_equal(self, data: Mapping, expected: Mapping, path: str) -> None: + for key, expected_value in expected.items(): + current_path = f"{path}.{key}" + self.assertIn(key, data, current_path) + actual_value = data[key] + if isinstance(expected_value, Mapping): + self.assertIsInstance(actual_value, Mapping, current_path) + self.assert_nested_equal(actual_value, expected_value, current_path) + else: + self.assertEqual(actual_value, expected_value, current_path) + + # ── MCP servers sync ─────────────────────────────────────────────────────── + + def test_mcp_servers_synced_to_claude_json(self) -> None: + """~/.claude.json should contain the filtered MCP servers.""" + _run_claude_sync(self.root, self.platform_cfg) + + data = self._read_json(self.home / ".claude.json") + self.assertIn("mcpServers", data) + self.assertIn("sample", data["mcpServers"]) + self.assertEqual(data["mcpServers"]["sample"]["command"], "echo") + self.assertEqual(data["mcpServers"]["sample"]["args"], ["hello"]) + + def test_claude_json_preserves_existing_top_level_keys(self) -> None: + """Existing top-level keys in ~/.claude.json must survive the sync.""" + (self.home / ".claude.json").parent.mkdir(parents=True, exist_ok=True) + self._write_json( + self.home / ".claude.json", + {"mcpServers": {}, "autoConnectIde": True, "customKey": "keep-me"}, + ) + + _run_claude_sync(self.root, self.platform_cfg) + + data = self._read_json(self.home / ".claude.json") + self.assertTrue(data.get("autoConnectIde")) + self.assertEqual(data.get("customKey"), "keep-me") + self.assertIn("sample", data["mcpServers"]) + + # ── settings.generated.json ───────────────────────────────────────────────── + + def test_settings_generated_json_contains_team_shared_keys(self) -> None: + """settings.generated.json must contain every team-shared key from claude.json.""" + _run_claude_sync(self.root, self.platform_cfg) + + generated = self._read_json(self.home / ".claude" / "settings.generated.json") + + # All non-internal, non-env, non-hooks, non-host keys must be present + team_shared_expected = { + "model": "claude-sonnet-4-6", + "effortLevel": "medium", + "alwaysThinkingEnabled": True, + "outputStyle": "Explanatory", + "includeGitInstructions": True, + "respectGitignore": True, + "fileCheckpointingEnabled": True, + "autoCompactEnabled": True, + "autoMemoryEnabled": True, + "respondToBashCommands": True, + } + for key, value in team_shared_expected.items(): + self.assertEqual(generated[key], value, f"key={key}") + + self.assert_nested_equal( + generated, + { + "permissions": { + "allow": [ + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git status *)", + "Bash(git branch *)", + ], + "deny": ["Bash(curl *)", "Bash(wget *)"], + "defaultMode": "default", + }, + }, + "generated", + ) + + def test_settings_generated_json_excludes_host_specific_keys(self) -> None: + """settings.generated.json must not leak host-specific keys.""" + _run_claude_sync(self.root, self.platform_cfg) + + generated = self._read_json(self.home / ".claude" / "settings.generated.json") + + # Enforce that NO host-specific key enters the generated file + for host_key in claude_module._HOST_SKIP: + self.assertNotIn( + host_key, + generated, + f"Host-specific key '{host_key}' leaked into settings.generated.json", + ) + + def test_settings_generated_json_excludes_internal_keys(self) -> None: + """Internal / handled-separately keys must not appear in generated JSON.""" + _run_claude_sync(self.root, self.platform_cfg) + + generated = self._read_json(self.home / ".claude" / "settings.generated.json") + + for excluded in ("_comment", "_hostSettings", "env", "hooks", "export_env_to_zshrc"): + self.assertNotIn( + excluded, generated, f"Key '{excluded}' should not be in settings.generated.json" + ) + + def test_settings_generated_json_skipped_when_no_team_shared_keys(self) -> None: + """When claude.json contains only env/hooks, generated file should be empty object.""" + minimal_cfg = { + "env": {"FOO": "bar"}, + "hooks": { + "SessionStart": [ + {"hooks": [{"type": "command", "command": "/bin/true", "timeout": 5}]} + ] + }, + } + _run_claude_sync(self.root, minimal_cfg) + + generated = self._read_json(self.home / ".claude" / "settings.generated.json") + self.assertEqual(generated, {}) + + # ── Env merge ────────────────────────────────────────────────────────────── + + def test_env_merged_into_settings_json(self) -> None: + """env vars from claude.json must be merged into ~/.claude/settings.json.""" + _run_claude_sync(self.root, self.platform_cfg) + + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertIn("env", settings) + # Secrets should have been resolved + self.assertEqual(settings["env"]["ANTHROPIC_AUTH_TOKEN"], "sk-ant-test-token") + self.assertEqual(settings["env"]["ANTHROPIC_BASE_URL"], "https://claude.example/v1") + self.assertEqual(settings["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], "claude-sonnet-4-6") + self.assertEqual(settings["env"]["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"], "1") + + def test_env_merge_preserves_existing_settings(self) -> None: + """Existing keys in settings.json (outside env/hooks) must be preserved.""" + (self.home / ".claude").mkdir(parents=True, exist_ok=True) + self._write_json( + self.home / ".claude" / "settings.json", + { + "env": {"EXISTING_VAR": "existing-value"}, + "theme": "light", + "editorMode": "vim", + }, + ) + + _run_claude_sync(self.root, self.platform_cfg) + + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertEqual(settings["theme"], "light") + self.assertEqual(settings["editorMode"], "vim") + # Existing env should be preserved alongside new ones + self.assertEqual(settings["env"]["EXISTING_VAR"], "existing-value") + self.assertEqual(settings["env"]["ANTHROPIC_AUTH_TOKEN"], "sk-ant-test-token") + + def test_env_merge_skips_when_no_env_in_cfg(self) -> None: + """When platform cfg has no env, settings.json env should be untouched.""" + (self.home / ".claude").mkdir(parents=True, exist_ok=True) + self._write_json( + self.home / ".claude" / "settings.json", + {"env": {"KEEP": "me"}, "editorMode": "vim"}, + ) + + cfg_no_env = dict(self.platform_cfg) + del cfg_no_env["env"] + _run_claude_sync(self.root, cfg_no_env) + + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertEqual(settings["env"], {"KEEP": "me"}) + self.assertEqual(settings["editorMode"], "vim") + + # ── Hooks merge ──────────────────────────────────────────────────────────── + + def test_hooks_expanded_and_merged(self) -> None: + """Hook paths should be expanded and merged into settings.json.""" + _run_claude_sync(self.root, self.platform_cfg) + + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertIn("hooks", settings) + self.assertIn("SessionStart", settings["hooks"]) + session_hooks = settings["hooks"]["SessionStart"][0]["hooks"] + self.assertEqual(session_hooks[0]["type"], "command") + self.assertEqual(session_hooks[0]["timeout"], 10) + # Path should be expanded with the patched HOME + expected_command = str(Path(self.home / ".claude" / "hooks" / "xmcp-init.sh")) + self.assertEqual(session_hooks[0]["command"], expected_command) + + def test_hooks_merge_preserves_existing_hooks(self) -> None: + """Existing hook events in settings.json should survive the merge.""" + (self.home / ".claude").mkdir(parents=True, exist_ok=True) + self._write_json( + self.home / ".claude" / "settings.json", + { + "hooks": { + "PostToolUse": [ + {"hooks": [{"type": "command", "command": "/bin/echo", "timeout": 5}]} + ] + } + }, + ) + + _run_claude_sync(self.root, self.platform_cfg) + + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertIn("PostToolUse", settings["hooks"]) + self.assertIn("SessionStart", settings["hooks"]) + + # ── Hook script installation ─────────────────────────────────────────────── + + def test_hook_scripts_installed(self) -> None: + """Hook scripts from repo hooks/ should be copied to ~/.claude/hooks/.""" + # Write a real script into repo hooks/ for this test + hook_src = self._repo_hooks_dir / "xmcp-init.sh" + hook_src.write_text("#!/bin/bash\necho 'hello'\n", encoding="utf-8") + hook_src.chmod(0o755) + + _run_claude_sync(self.root, self.platform_cfg) + + dest = self.home / ".claude" / "hooks" / "xmcp-init.sh" + self.assertTrue(dest.exists(), f"Hook script not installed at {dest}") + self.assertEqual(dest.read_text(encoding="utf-8"), "#!/bin/bash\necho 'hello'\n") + # Verify executable permission + self.assertTrue(dest.stat().st_mode & stat.S_IXUSR, f"{dest} is not executable") + + # ── Xcode Claude Agent ───────────────────────────────────────────────────── + + def test_xcode_claude_json_root_mcp_servers(self) -> None: + """Xcode config without projects should get MCP servers at root level.""" + xc_path = self.home / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json" + xc_path.parent.mkdir(parents=True, exist_ok=True) + # Pre-existing data without projects key + data = {"mcpServers": {}, "existingKey": "keep"} + self._write_json(xc_path, data) + + _run_claude_sync(self.root, self.platform_cfg) + + result = self._read_json(xc_path) + self.assertEqual(result.get("existingKey"), "keep") + self.assertIn("sample", result["mcpServers"]) + + def test_xcode_claude_json_per_project_mcp_servers(self) -> None: + """Xcode config with projects should inject MCP servers per project.""" + xc_path = self.home / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json" + xc_path.parent.mkdir(parents=True, exist_ok=True) + data = { + "projects": { + "/Users/test/project1": {"otherKey": "val1"}, + "/Users/test/project2": {"otherKey": "val2"}, + } + } + self._write_json(xc_path, data) + + _run_claude_sync(self.root, self.platform_cfg) + + result = self._read_json(xc_path) + for proj_name in ("/Users/test/project1", "/Users/test/project2"): + self.assertIn("mcpServers", result["projects"][proj_name]) + self.assertIn("sample", result["projects"][proj_name]["mcpServers"]) + # Root level should NOT have mcpServers when projects exist + self.assertNotIn("mcpServers", result) + + # ── Full property mapping ────────────────────────────────────────────────── + + def test_claude_json_properties_are_mapped_or_excluded_as_expected(self) -> None: + """Every key in claude.json must be accounted for: mapped or explicitly excluded.""" + # Define the expected complete key set from claude.json + covered_keys = { + "_comment", + "model", + "effortLevel", + "alwaysThinkingEnabled", + "outputStyle", + "includeGitInstructions", + "respectGitignore", + "fileCheckpointingEnabled", + "autoCompactEnabled", + "autoMemoryEnabled", + "respondToBashCommands", + "env", + "permissions", + "hooks", + "_hostSettings", + } + self.assertEqual(set(self.platform_cfg), covered_keys, "claude.json keys changed — update tests") + + _run_claude_sync(self.root, self.platform_cfg) + + generated = self._read_json(self.home / ".claude" / "settings.generated.json") + + # ── Keys expected in generated ── + self.assertEqual(generated["model"], "claude-sonnet-4-6") + self.assertEqual(generated["effortLevel"], "medium") + self.assertTrue(generated["alwaysThinkingEnabled"]) + self.assertEqual(generated["outputStyle"], "Explanatory") + self.assertTrue(generated["includeGitInstructions"]) + self.assertTrue(generated["respectGitignore"]) + self.assertTrue(generated["fileCheckpointingEnabled"]) + self.assertTrue(generated["autoCompactEnabled"]) + self.assertTrue(generated["autoMemoryEnabled"]) + self.assertTrue(generated["respondToBashCommands"]) + self.assertEqual(generated["permissions"]["defaultMode"], "default") + + # ── Keys excluded from generated ── + for excluded_key in ("_comment", "_hostSettings", "env", "hooks", "export_env_to_zshrc"): + self.assertNotIn(excluded_key, generated) + + # ── Verify settings.json has env and hooks ── + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertIn("env", settings) + self.assertIn("hooks", settings) + + # ── generate_managed_settings unit test ───────────────────────────────────── + + def test_generate_managed_settings_filters_correctly(self) -> None: + """Unit test for generate_managed_settings() filtering logic.""" + cfg = { + "model": "claude-opus-4-8", + "effortLevel": "high", + "env": {"FOO": "bar"}, + "hooks": {"SessionStart": []}, + "_comment": "test comment", + "_hostSettings": {"theme": "dark"}, + "export_env_to_zshrc": {"KEY": "val"}, + "theme": "light", + "editorMode": "vim", + "autoConnectIde": False, + } + managed = claude_module.generate_managed_settings(cfg) + + # Included + self.assertEqual(managed["model"], "claude-opus-4-8") + self.assertEqual(managed["effortLevel"], "high") + + # Excluded + for excluded in ( + "env", + "hooks", + "_comment", + "_hostSettings", + "export_env_to_zshrc", + "theme", + "editorMode", + "autoConnectIde", + ): + self.assertNotIn(excluded, managed, f"'{excluded}' should be excluded") + + def test_generate_managed_settings_deep_copies(self) -> None: + """Modifying the result must NOT affect the original config.""" + cfg = {"permissions": {"allow": ["Bash(git *)"]}} + managed = claude_module.generate_managed_settings(cfg) + managed["permissions"]["allow"].append("WebFetch") + + self.assertEqual( + cfg["permissions"]["allow"], + ["Bash(git *)"], + "Original config was mutated — generate_managed_settings must deep copy", + ) + + # ── Edge cases ────────────────────────────────────────────────────────────── + + def test_claude_json_not_found_graceful(self) -> None: + """When claude.json doesn't exist, sync should not crash.""" + if (self.root / "env" / "platforms" / "claude.json").exists(): + (self.root / "env" / "platforms" / "claude.json").unlink() + + # sync_config.load_platform_config returns {} for missing file + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "claude"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + + # No crash; claude.json should still be created with MCP servers + data = self._read_json(self.home / ".claude.json") + self.assertIn("mcpServers", data) + + +if __name__ == "__main__": + unittest.main() From 3fc357bbfd3a0a0f93c17920b25e8b9c1cfa82b6 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 16:04:21 +0800 Subject: [PATCH 10/30] feat(sync): enhance Xcode Claude Agent settings synchronization - Added a new function to sync team-shared settings, environment variables, and hooks into the Xcode Claude Agent configuration directory. - Introduced a method to remove obsolete generated settings files, ensuring cleaner configuration management. - Updated the sync process to reflect changes in settings handling, merging team-shared keys into the appropriate settings.json file. - Refactored tests to validate the new synchronization logic and ensure proper handling of team-shared keys and environment variables. --- sync/platforms/claude.py | 65 ++++++++++++++--- tests/test_claude_sync.py | 147 +++++++++++++++++++++++++++----------- 2 files changed, 161 insertions(+), 51 deletions(-) diff --git a/sync/platforms/claude.py b/sync/platforms/claude.py index b2b8d0c..fc63f14 100644 --- a/sync/platforms/claude.py +++ b/sync/platforms/claude.py @@ -27,6 +27,11 @@ def xcode_claude_json_path() -> Path: return Path.home() / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude.json" +def xcode_claude_dir() -> Path: + """Xcode Claude Agent .claude config directory (settings + hooks).""" + return Path.home() / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude" + + def _repo_hooks_dir() -> Path: return Path(__file__).resolve().parents[2] / "hooks" @@ -168,14 +173,51 @@ def _sync_xcode_claude_json(servers: dict[str, Any]) -> None: print(f"Replaced MCP servers in {path} ({mode}).") +def _sync_xcode_claude_settings( + managed: dict[str, Any], env: dict[str, Any], hooks: dict[str, Any] +) -> None: + """Sync team-shared settings, env, and hooks to Xcode Claude Agent dir.""" + xc_dir = xcode_claude_dir() + xc_dir.mkdir(parents=True, exist_ok=True) + _remove_obsolete_generated_settings(xc_dir / "settings.generated.json") + + if managed: + print(f"Prepared Xcode Claude settings ({len(managed)} team-shared keys).") + + # Merge env and hooks into Xcode settings.json + settings_path = xc_dir / "settings.json" + settings = read_json_object(settings_path) + + if managed: + settings = merge_object(settings, managed) + + if isinstance(env, dict) and env: + settings["env"] = merge_object(settings.get("env"), env) + print(f"Merged env into Xcode {settings_path} ({len(env)} vars).") + + if hooks: + existing = settings.get("hooks", {}) + existing.update(hooks) + settings["hooks"] = existing + print(f"Merged hooks into Xcode {settings_path} ({len(hooks)} event(s)).") + + write_json(settings_path, settings) + + +def _remove_obsolete_generated_settings(path: Path) -> None: + if path.exists(): + path.unlink() + print(f"Removed obsolete generated settings file: {path}") + + def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: """Sync MCP servers and Claude Code platform config. Steps: 1. Write ~/.claude.json with MCP servers (preserving other top-level keys). - 2. Sync Xcode Claude Agent config. - 3. Generate ~/.claude/settings.generated.json for team-shared settings. - 4. Merge env and hooks into ~/.claude/settings.json. + 2. Sync MCP servers to Xcode Claude Agent (.claude.json). + 3. Merge team-shared settings, env, and hooks into ~/.claude/settings.json. + 4. Sync team-shared settings, env, and hooks to Xcode Claude Agent. 5. Install hook shell scripts. """ # ── 1. ~/.claude.json — MCP servers ── @@ -188,20 +230,20 @@ def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: # ── 2. Xcode Claude Agent ── _sync_xcode_claude_json(mcp_servers) - # ── 3. settings.generated.json — team-shared settings ── + # ── 3. settings.json — merge team-shared settings, env, and hooks ── managed = generate_managed_settings(cfg) - gen_path = claude_settings_generated_json_path() - gen_path.parent.mkdir(parents=True, exist_ok=True) - write_json(gen_path, managed) + _remove_obsolete_generated_settings(claude_settings_generated_json_path()) if managed: - print(f"Wrote {gen_path} ({len(managed)} team-shared keys).") + print(f"Prepared Claude settings ({len(managed)} team-shared keys).") else: - print(f"[claude] No team-shared settings to generate — {gen_path} unchanged.") + print("[claude] No team-shared settings in platform config.") - # ── 4. settings.json — merge env and hooks into user settings ── settings_path = claude_settings_json_path() settings = read_json_object(settings_path) + if managed: + settings = merge_object(settings, managed) + # 4a. Merge env env = cfg.get("env", {}) if isinstance(env, dict) and env: @@ -222,3 +264,6 @@ def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: print(f"Merged hooks into {settings_path} ({len(config_hooks)} event(s)).") write_json(settings_path, settings) + + # ── 5. Xcode Claude Agent — settings ── + _sync_xcode_claude_settings(managed, env, config_hooks) diff --git a/tests/test_claude_sync.py b/tests/test_claude_sync.py index 9561092..37f6ae8 100644 --- a/tests/test_claude_sync.py +++ b/tests/test_claude_sync.py @@ -146,13 +146,13 @@ def test_claude_json_preserves_existing_top_level_keys(self) -> None: self.assertEqual(data.get("customKey"), "keep-me") self.assertIn("sample", data["mcpServers"]) - # ── settings.generated.json ───────────────────────────────────────────────── + # ── settings.json team-shared settings ─────────────────────────────────────── - def test_settings_generated_json_contains_team_shared_keys(self) -> None: - """settings.generated.json must contain every team-shared key from claude.json.""" + def test_settings_json_contains_team_shared_keys(self) -> None: + """settings.json must contain every team-shared key from claude.json.""" _run_claude_sync(self.root, self.platform_cfg) - generated = self._read_json(self.home / ".claude" / "settings.generated.json") + settings = self._read_json(self.home / ".claude" / "settings.json") # All non-internal, non-env, non-hooks, non-host keys must be present team_shared_expected = { @@ -168,10 +168,10 @@ def test_settings_generated_json_contains_team_shared_keys(self) -> None: "respondToBashCommands": True, } for key, value in team_shared_expected.items(): - self.assertEqual(generated[key], value, f"key={key}") + self.assertEqual(settings[key], value, f"key={key}") self.assert_nested_equal( - generated, + settings, { "permissions": { "allow": [ @@ -184,36 +184,45 @@ def test_settings_generated_json_contains_team_shared_keys(self) -> None: "defaultMode": "default", }, }, - "generated", + "settings", ) - def test_settings_generated_json_excludes_host_specific_keys(self) -> None: - """settings.generated.json must not leak host-specific keys.""" + def test_settings_json_excludes_host_specific_keys(self) -> None: + """settings.json must not receive host-specific keys from claude.json.""" _run_claude_sync(self.root, self.platform_cfg) - generated = self._read_json(self.home / ".claude" / "settings.generated.json") + settings = self._read_json(self.home / ".claude" / "settings.json") - # Enforce that NO host-specific key enters the generated file + # Enforce that NO host-specific key is introduced by platform config sync for host_key in claude_module._HOST_SKIP: self.assertNotIn( host_key, - generated, - f"Host-specific key '{host_key}' leaked into settings.generated.json", + settings, + f"Host-specific key '{host_key}' leaked into settings.json", ) - def test_settings_generated_json_excludes_internal_keys(self) -> None: - """Internal / handled-separately keys must not appear in generated JSON.""" + def test_settings_json_excludes_internal_keys(self) -> None: + """Internal-only keys must not appear in settings.json.""" _run_claude_sync(self.root, self.platform_cfg) - generated = self._read_json(self.home / ".claude" / "settings.generated.json") + settings = self._read_json(self.home / ".claude" / "settings.json") - for excluded in ("_comment", "_hostSettings", "env", "hooks", "export_env_to_zshrc"): + for excluded in ("_comment", "_hostSettings", "export_env_to_zshrc"): self.assertNotIn( - excluded, generated, f"Key '{excluded}' should not be in settings.generated.json" + excluded, settings, f"Key '{excluded}' should not be in settings.json" ) - def test_settings_generated_json_skipped_when_no_team_shared_keys(self) -> None: - """When claude.json contains only env/hooks, generated file should be empty object.""" + def test_obsolete_settings_generated_json_removed(self) -> None: + """Old settings.generated.json should be removed because Claude Code does not load it.""" + generated_path = self.home / ".claude" / "settings.generated.json" + self._write_json(generated_path, {"model": "stale"}) + + _run_claude_sync(self.root, self.platform_cfg) + + self.assertFalse(generated_path.exists()) + + def test_settings_json_has_no_team_shared_keys_when_cfg_only_has_env_hooks(self) -> None: + """When claude.json contains only env/hooks, settings.json only receives env/hooks.""" minimal_cfg = { "env": {"FOO": "bar"}, "hooks": { @@ -224,8 +233,10 @@ def test_settings_generated_json_skipped_when_no_team_shared_keys(self) -> None: } _run_claude_sync(self.root, minimal_cfg) - generated = self._read_json(self.home / ".claude" / "settings.generated.json") - self.assertEqual(generated, {}) + settings = self._read_json(self.home / ".claude" / "settings.json") + self.assertNotIn("model", settings) + self.assertEqual(settings["env"], {"FOO": "bar"}) + self.assertIn("SessionStart", settings["hooks"]) # ── Env merge ────────────────────────────────────────────────────────────── @@ -368,6 +379,61 @@ def test_xcode_claude_json_per_project_mcp_servers(self) -> None: # Root level should NOT have mcpServers when projects exist self.assertNotIn("mcpServers", result) + # ── Xcode Claude Agent settings ──────────────────────────────────────────── + + def test_xcode_claude_settings_receive_team_shared_keys(self) -> None: + """Team-shared keys must be merged into Xcode Claude Agent settings.json.""" + xc_dir = self.home / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude" + self._write_json(xc_dir / "settings.generated.json", {"model": "stale"}) + + _run_claude_sync(self.root, self.platform_cfg) + + xc_settings = xc_dir / "settings.json" + self.assertTrue(xc_settings.exists(), f"Missing {xc_settings}") + settings = self._read_json(xc_settings) + + self.assertEqual(settings["model"], "claude-sonnet-4-6") + self.assertTrue(settings["alwaysThinkingEnabled"]) + self.assertEqual(settings["permissions"]["defaultMode"], "default") + self.assertFalse((xc_settings.parent / "settings.generated.json").exists()) + + # Should NOT leak host-specific keys + for host_key in claude_module._HOST_SKIP: + self.assertNotIn(host_key, settings, f"Host key '{host_key}' leaked into Xcode settings") + + def test_xcode_claude_settings_env_merged(self) -> None: + """env vars must be merged into the Xcode Claude Agent settings.json.""" + # Pre-seed Xcode settings.json with some existing content + xc_settings_dir = self.home / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude" + xc_settings_dir.mkdir(parents=True, exist_ok=True) + self._write_json( + xc_settings_dir / "settings.json", + {"env": {"XC_LEGACY": "keep-me"}}, + ) + + _run_claude_sync(self.root, self.platform_cfg) + + settings = self._read_json(xc_settings_dir / "settings.json") + self.assertEqual(settings["env"]["XC_LEGACY"], "keep-me") + self.assertEqual(settings["env"]["ANTHROPIC_AUTH_TOKEN"], "sk-ant-test-token") + self.assertEqual(settings["env"]["ANTHROPIC_BASE_URL"], "https://claude.example/v1") + + def test_xcode_claude_settings_hooks_merged(self) -> None: + """hooks must be merged into the Xcode Claude Agent settings.json.""" + _run_claude_sync(self.root, self.platform_cfg) + + xc_settings = self.home / "Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/.claude/settings.json" + self.assertTrue(xc_settings.exists(), f"Missing {xc_settings}") + settings = self._read_json(xc_settings) + + self.assertIn("hooks", settings) + self.assertIn("SessionStart", settings["hooks"]) + session_hooks = settings["hooks"]["SessionStart"][0]["hooks"] + self.assertEqual(session_hooks[0]["type"], "command") + self.assertEqual(session_hooks[0]["timeout"], 10) + expected_command = str(Path(self.home / ".claude" / "hooks" / "xmcp-init.sh")) + self.assertEqual(session_hooks[0]["command"], expected_command) + # ── Full property mapping ────────────────────────────────────────────────── def test_claude_json_properties_are_mapped_or_excluded_as_expected(self) -> None: @@ -394,27 +460,26 @@ def test_claude_json_properties_are_mapped_or_excluded_as_expected(self) -> None _run_claude_sync(self.root, self.platform_cfg) - generated = self._read_json(self.home / ".claude" / "settings.generated.json") - - # ── Keys expected in generated ── - self.assertEqual(generated["model"], "claude-sonnet-4-6") - self.assertEqual(generated["effortLevel"], "medium") - self.assertTrue(generated["alwaysThinkingEnabled"]) - self.assertEqual(generated["outputStyle"], "Explanatory") - self.assertTrue(generated["includeGitInstructions"]) - self.assertTrue(generated["respectGitignore"]) - self.assertTrue(generated["fileCheckpointingEnabled"]) - self.assertTrue(generated["autoCompactEnabled"]) - self.assertTrue(generated["autoMemoryEnabled"]) - self.assertTrue(generated["respondToBashCommands"]) - self.assertEqual(generated["permissions"]["defaultMode"], "default") - - # ── Keys excluded from generated ── - for excluded_key in ("_comment", "_hostSettings", "env", "hooks", "export_env_to_zshrc"): - self.assertNotIn(excluded_key, generated) + settings = self._read_json(self.home / ".claude" / "settings.json") + + # ── Team-shared keys expected in settings.json ── + self.assertEqual(settings["model"], "claude-sonnet-4-6") + self.assertEqual(settings["effortLevel"], "medium") + self.assertTrue(settings["alwaysThinkingEnabled"]) + self.assertEqual(settings["outputStyle"], "Explanatory") + self.assertTrue(settings["includeGitInstructions"]) + self.assertTrue(settings["respectGitignore"]) + self.assertTrue(settings["fileCheckpointingEnabled"]) + self.assertTrue(settings["autoCompactEnabled"]) + self.assertTrue(settings["autoMemoryEnabled"]) + self.assertTrue(settings["respondToBashCommands"]) + self.assertEqual(settings["permissions"]["defaultMode"], "default") + + # ── Internal-only keys excluded from settings.json ── + for excluded_key in ("_comment", "_hostSettings", "export_env_to_zshrc"): + self.assertNotIn(excluded_key, settings) # ── Verify settings.json has env and hooks ── - settings = self._read_json(self.home / ".claude" / "settings.json") self.assertIn("env", settings) self.assertIn("hooks", settings) From 46229afab4a58f4ee3021f4761dea82328754fc8 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 16:30:10 +0800 Subject: [PATCH 11/30] feat(gemini): enhance Gemini platform configuration and synchronization - Expanded `gemini.json` with detailed settings for model behavior, context management, and security features. - Introduced new functions in `gemini.py` to extract and deep-merge settings, ensuring developer customizations are preserved during synchronization. - Added support for syncing Gemini settings to both native CLI and Xcode CodingAssistant directories. - Improved path handling in `common.py` for Gemini-specific directories and settings file management. --- env/platforms/gemini.json | 52 +++++- sync/platforms/common.py | 8 + sync/platforms/gemini.py | 65 ++++++- tests/test_gemini_sync.py | 381 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 498 insertions(+), 8 deletions(-) create mode 100644 tests/test_gemini_sync.py diff --git a/env/platforms/gemini.json b/env/platforms/gemini.json index 48cadf8..ecfccf6 100644 --- a/env/platforms/gemini.json +++ b/env/platforms/gemini.json @@ -1,5 +1,55 @@ { - "env": { + "_comment": "Gemini CLI platform configuration. Schema: https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/config/settingsSchema.ts", + "model": { + "name": "gemini-3.5-flash", + "maxSessionTurns": -1, + "compressionThreshold": 0.5, + "skipNextSpeakerCheck": true + }, + "context": { + "fileName": ["GEMINI.md", "CODEBUDDY.md", "CLAUDE.md"], + "includeDirectoryTree": true, + "importFormat": "tree", + "fileFiltering": { + "respectGitIgnore": true, + "respectGeminiIgnore": true, + "enableFileWatcher": true, + "enableRecursiveFileSearch": true, + "enableFuzzySearch": true + } + }, + "tools": { + "sandbox": "workspace-write", + "sandboxNetworkAccess": true, + "useRipgrep": true, + "shell": { + "enableInteractiveShell": true + } + }, + "skills": { + "enabled": true + }, + "hooksConfig": { + "enabled": true + }, + "security": { + "folderTrust": { + "enabled": true + } + }, + "experimental": { + "directWebFetch": true, + "enableAgents": true, + "autoMemory": true, + "contextManagement": true + }, + "contextManagement": { + "historyWindow": { + "maxTokens": 200000, + "retainedTokens": 10000 + } + }, + "export_env_to_zshrc": { "GEMINI_API_KEY": "${gemini.key}", "GOOGLE_GEMINI_BASE_URL": "${gemini.url}", "GEMINI_MODEL": "gemini-3.5-flash" diff --git a/sync/platforms/common.py b/sync/platforms/common.py index aaf64fd..8adce4d 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -295,6 +295,14 @@ def xcode_codex_dir() -> Path: return Path.home() / "Library/Developer/Xcode/CodingAssistant/codex" +def xcode_gemini_dir() -> Path: + return Path.home() / "Library/Developer/Xcode/CodingAssistant/gemini" + + +def gemini_settings_path() -> Path: + return Path.home() / ".gemini/settings.json" + + # ── TOML generation utilities ──────────────────────────────────────────────── def toml_quote(s: str) -> str: diff --git a/sync/platforms/gemini.py b/sync/platforms/gemini.py index cf83dd0..06a2277 100644 --- a/sync/platforms/gemini.py +++ b/sync/platforms/gemini.py @@ -1,14 +1,65 @@ from pathlib import Path from typing import Any -from .common import sync_env_to_zshrc, sync_json_mcp +from .common import gemini_settings_path, read_json_object, write_json, xcode_gemini_dir -_TARGET = Path.home() / ".gemini/settings.json" +# Internal/platform keys that should NOT appear in the managed settings.json. +# These are consumed by the sync engine/orchestrator, not by Gemini CLI itself. +_INTERNAL_SKIP = {"export_env_to_zshrc", "_comment"} + + +def _extract_settings(cfg: dict[str, Any]) -> dict[str, Any]: + """Extract Gemini CLI settings from platform config, stripping internal keys.""" + return {k: v for k, v in cfg.items() if k not in _INTERNAL_SKIP} + + +def _deep_merge(existing: dict[str, Any], managed: dict[str, Any]) -> dict[str, Any]: + """Deep-merge managed settings into existing, preserving per-developer customizations. + + Dict values are merged recursively, with managed values overriding existing + values at the same path. This lets developers keep custom sibling keys + inside nested Gemini settings objects while the shared config still wins + for managed fields. + """ + result = dict(existing) + for key, value in managed.items(): + if isinstance(value, dict) and isinstance(result.get(key), dict): + result[key] = _deep_merge(result[key], value) + else: + result[key] = value + return result + + +def _sync_settings(path: Path, managed_settings: dict[str, Any], mcp_servers: dict[str, Any]) -> None: + """Write managed settings + MCP servers into a Gemini settings.json target. + + Preserves any developer-added keys by deep-merging managed settings + on top of the existing file. + """ + existing = read_json_object(path) + merged = _deep_merge(existing, managed_settings) + merged["mcpServers"] = mcp_servers + write_json(path, merged) + print(f"Synced Gemini settings to {path}.") def sync(mcp_servers: dict[str, Any], cfg: dict[str, Any]) -> None: - """Sync MCP servers and env vars to Gemini CLI.""" - sync_json_mcp(_TARGET, mcp_servers) - env = cfg.get("env", {}) - if isinstance(env, dict) and env: - sync_env_to_zshrc("gemini", env) + """Sync MCP servers and platform config to Gemini CLI (native + Xcode). + + Writes to: + - ~/.gemini/settings.json (Gemini CLI native config) + - ~/Library/Developer/Xcode/CodingAssistant/gemini/settings.json (Xcode target) + + Environment variables (GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL) are exported + to ~/.zshrc by the orchestrator via the export_env_to_zshrc mechanism + defined in env/platforms/gemini.json. + """ + managed = _extract_settings(cfg) + + # ── Native Gemini CLI target ── + _sync_settings(gemini_settings_path(), managed, mcp_servers) + + # ── Xcode CodingAssistant target ── + xc = xcode_gemini_dir() + xc.mkdir(parents=True, exist_ok=True) + _sync_settings(xc / "settings.json", managed, mcp_servers) diff --git a/tests/test_gemini_sync.py b/tests/test_gemini_sync.py new file mode 100644 index 0000000..6ef6b26 --- /dev/null +++ b/tests/test_gemini_sync.py @@ -0,0 +1,381 @@ +import contextlib +import io +import json +import os +import sys +import tempfile +import unittest +from collections.abc import Mapping +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SYNC_DIR = REPO_ROOT / "sync" +if str(SYNC_DIR) not in sys.path: + sys.path.insert(0, str(SYNC_DIR)) + +import sync_config # noqa: E402 +from platforms import common # noqa: E402 + + +@contextlib.contextmanager +def patched_sync_environment(root: Path): + """Redirect HOME and module-level paths for isolated Gemini sync tests.""" + old_env = {k: os.environ.get(k) for k in ("HOME",)} + old_paths = (common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH) + old_argv = sys.argv[:] + try: + os.environ["HOME"] = str(root / "home") + common.MCP_DIR = root / "env" / "mcp" + common.PLATFORMS_DIR = root / "env" / "platforms" + common.SECRETS_PATH = root / "env" / "secrets.json" + yield + finally: + for key, value in old_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH = old_paths + sys.argv = old_argv + + +class GeminiSyncTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + self.platform_cfg = json.loads( + (REPO_ROOT / "env" / "platforms" / "gemini.json").read_text() + ) + self._write_json( + self.root / "env" / "mcp" / "sample.json", + { + "name": "sample", + "type": "stdio", + "command": "echo", + "args": ["hello"], + "platforms": ["gemini"], + }, + ) + self._write_json( + self.root / "env" / "secrets.json", + {"gemini": {"url": "https://generativelanguage.googleapis.com", "key": "sk-test-gemini"}}, + ) + + def tearDown(self) -> None: + self.tmp.cleanup() + + def _write_json(self, path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8") + + def _read_json(self, path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + def _run_gemini_sync(self, cfg: dict | None = None) -> dict: + """Run Gemini sync and return the parsed settings.json content.""" + target_cfg = cfg if cfg is not None else self.platform_cfg + self._write_json(self.root / "env" / "platforms" / "gemini.json", target_cfg) + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "gemini"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + return self._read_json(self.root / "home" / ".gemini" / "settings.json") + + def assert_nested_equal(self, data: Mapping, expected: Mapping, path: str) -> None: + for key, expected_value in expected.items(): + current_path = f"{path}.{key}" + self.assertIn(key, data, current_path) + actual_value = data[key] + if isinstance(expected_value, Mapping): + self.assertIsInstance(actual_value, Mapping, current_path) + self.assert_nested_equal(actual_value, expected_value, current_path) + else: + self.assertEqual(actual_value, expected_value, current_path) + + # ── MCP servers ────────────────────────────────────────────────────────── + + def test_mcp_servers_synced_to_settings_json(self) -> None: + settings = self._run_gemini_sync() + + self.assertIn("mcpServers", settings) + self.assertIn("sample", settings["mcpServers"]) + self.assertEqual(settings["mcpServers"]["sample"]["command"], "echo") + self.assertEqual(settings["mcpServers"]["sample"]["args"], ["hello"]) + + # ── Managed settings ───────────────────────────────────────────────────── + + def test_settings_json_contains_managed_keys(self) -> None: + settings = self._run_gemini_sync() + + self.assert_nested_equal( + settings, + { + "model": { + "name": "gemini-3.5-flash", + "maxSessionTurns": -1, + "compressionThreshold": 0.5, + "skipNextSpeakerCheck": True, + }, + }, + "settings", + ) + self.assertEqual(settings["context"]["fileName"], ["GEMINI.md", "CODEBUDDY.md", "CLAUDE.md"]) + self.assertTrue(settings["context"]["includeDirectoryTree"]) + self.assertEqual(settings["context"]["importFormat"], "tree") + self.assertTrue(settings["tools"]["useRipgrep"]) + self.assertEqual(settings["tools"]["sandbox"], "workspace-write") + self.assertTrue(settings["tools"]["sandboxNetworkAccess"]) + self.assertTrue(settings["tools"]["shell"]["enableInteractiveShell"]) + self.assertTrue(settings["skills"]["enabled"]) + self.assertTrue(settings["hooksConfig"]["enabled"]) + self.assertTrue(settings["security"]["folderTrust"]["enabled"]) + self.assertTrue(settings["experimental"]["directWebFetch"]) + self.assertTrue(settings["experimental"]["enableAgents"]) + self.assertTrue(settings["experimental"]["autoMemory"]) + self.assertTrue(settings["experimental"]["contextManagement"]) + + def test_settings_json_excludes_internal_keys(self) -> None: + settings = self._run_gemini_sync() + + # Internal keys must NOT leak into settings.json + self.assertNotIn("_comment", settings) + self.assertNotIn("export_env_to_zshrc", settings) + + def test_settings_json_preserves_existing_user_keys(self) -> None: + """User-added keys outside the managed set are preserved after sync.""" + settings_path = self.root / "home" / ".gemini" / "settings.json" + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text( + json.dumps( + { + "ui": {"theme": "dark", "hideBanner": True}, + "general": {"preferredEditor": "cursor"}, + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + settings = self._run_gemini_sync() + + self.assertEqual(settings["ui"]["theme"], "dark") + self.assertTrue(settings["ui"]["hideBanner"]) + self.assertEqual(settings["general"]["preferredEditor"], "cursor") + # Managed keys should also be present + self.assertEqual(settings["model"]["name"], "gemini-3.5-flash") + self.assertIn("mcpServers", settings) + + def test_settings_json_deep_merges_nested_user_keys(self) -> None: + """Nested dicts are recursively merged: user sub-keys preserved, managed overrides applied.""" + settings_path = self.root / "home" / ".gemini" / "settings.json" + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text( + json.dumps( + { + "model": {"maxSessionTurns": 50, "customUserField": "keep-me"}, + "context": { + "fileFiltering": { + "respectGitIgnore": False, + "customUserFilter": "keep-me", + } + }, + "tools": { + "shell": {"customShellSetting": "keep-me"}, + "customToolSetting": True, + }, + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + settings = self._run_gemini_sync() + + # Managed value overrides existing + self.assertEqual(settings["model"]["maxSessionTurns"], -1) + # User custom fields preserved + self.assertEqual(settings["model"]["customUserField"], "keep-me") + # User custom top-level sub-keys preserved + self.assertTrue(settings["tools"]["customToolSetting"]) + # User custom nested sub-keys preserved + self.assertEqual(settings["context"]["fileFiltering"]["customUserFilter"], "keep-me") + self.assertEqual(settings["tools"]["shell"]["customShellSetting"], "keep-me") + # Managed nested values still override existing values at the same path + self.assertTrue(settings["context"]["fileFiltering"]["respectGitIgnore"]) + + # ── Xcode target ───────────────────────────────────────────────────────── + + def test_xcode_target_receives_same_settings(self) -> None: + self._run_gemini_sync() + + xc_settings_path = ( + self.root + / "home" + / "Library" + / "Developer" + / "Xcode" + / "CodingAssistant" + / "gemini" + / "settings.json" + ) + self.assertTrue(xc_settings_path.exists(), "Xcode Gemini settings.json was not created") + + native = self._read_json(self.root / "home" / ".gemini" / "settings.json") + xcode = self._read_json(xc_settings_path) + self.assertEqual(native, xcode) + + def test_xcode_target_preserves_existing_user_keys(self) -> None: + xc_settings_path = ( + self.root + / "home" + / "Library" + / "Developer" + / "Xcode" + / "CodingAssistant" + / "gemini" + / "settings.json" + ) + self._write_json( + xc_settings_path, + { + "ui": {"theme": "dark"}, + "context": {"fileFiltering": {"customXcodeFilter": "keep-me"}}, + }, + ) + + xcode = self._run_gemini_sync() + xcode = self._read_json(xc_settings_path) + + self.assertEqual(xcode["ui"]["theme"], "dark") + self.assertEqual(xcode["context"]["fileFiltering"]["customXcodeFilter"], "keep-me") + self.assertTrue(xcode["context"]["fileFiltering"]["respectGitIgnore"]) + self.assertIn("mcpServers", xcode) + + # ── zshrc env export ───────────────────────────────────────────────────── + + def test_export_env_to_zshrc_creates_managed_block(self) -> None: + cfg = dict(self.platform_cfg) + cfg["export_env_to_zshrc"] = { + "GEMINI_API_KEY": "sk-test-gemini", + "GOOGLE_GEMINI_BASE_URL": "https://generativelanguage.googleapis.com", + "GEMINI_MODEL": "gemini-3.5-flash", + } + + self._run_gemini_sync(cfg) + + zshrc = self.root / "home" / ".zshrc" + self.assertTrue(zshrc.exists()) + zshrc_text = zshrc.read_text(encoding="utf-8") + + self.assertIn("# BEGIN GEMINI ENV SYNC (from env/platforms/gemini.json)", zshrc_text) + self.assertIn("export GEMINI_API_KEY=sk-test-gemini", zshrc_text) + self.assertIn("export GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com", zshrc_text) + self.assertIn("export GEMINI_MODEL=gemini-3.5-flash", zshrc_text) + self.assertIn("# END GEMINI ENV SYNC", zshrc_text) + + def test_export_env_to_zshrc_replaces_existing_block(self) -> None: + zshrc = self.root / "home" / ".zshrc" + zshrc.parent.mkdir(parents=True, exist_ok=True) + zshrc.write_text( + "before\n" + "# BEGIN GEMINI ENV SYNC (from env/platforms/gemini.json)\n" + "export GEMINI_API_KEY=old-key\n" + "export GEMINI_MODEL=old-model\n" + "# END GEMINI ENV SYNC\n" + "after\n", + encoding="utf-8", + ) + + cfg = dict(self.platform_cfg) + cfg["export_env_to_zshrc"] = { + "GEMINI_API_KEY": "sk-test-gemini", + "GOOGLE_GEMINI_BASE_URL": "https://generativelanguage.googleapis.com", + "GEMINI_MODEL": "gemini-3.5-flash", + } + self._run_gemini_sync(cfg) + + zshrc_text = zshrc.read_text(encoding="utf-8") + self.assertTrue(zshrc_text.startswith("before\n")) + self.assertTrue(zshrc_text.endswith("after\n")) + self.assertNotIn("old-key", zshrc_text) + self.assertNotIn("old-model", zshrc_text) + self.assertIn("export GEMINI_API_KEY=sk-test-gemini", zshrc_text) + self.assertIn("export GEMINI_MODEL=gemini-3.5-flash", zshrc_text) + # Block should appear exactly once + self.assertEqual(zshrc_text.count("# BEGIN GEMINI ENV SYNC"), 1) + self.assertEqual(zshrc_text.count("# END GEMINI ENV SYNC"), 1) + + # ── Property coverage ──────────────────────────────────────────────────── + + def test_gemini_json_properties_are_mapped_or_excluded_as_expected(self) -> None: + """Every key in gemini.json must be either in settings.json or in _INTERNAL_SKIP.""" + settings = self._run_gemini_sync() + + covered_keys = { + "model", + "context", + "tools", + "skills", + "hooksConfig", + "security", + "experimental", + "contextManagement", + "export_env_to_zshrc", + "_comment", + } + self.assertEqual(set(self.platform_cfg), covered_keys) + + # Keys that should appear in settings.json + managed_keys = covered_keys - {"export_env_to_zshrc", "_comment"} + for key in managed_keys: + self.assertIn(key, settings, f"Managed key '{key}' missing from settings.json") + + # Internal keys that should NOT appear + self.assertNotIn("_comment", settings) + self.assertNotIn("export_env_to_zshrc", settings) + + # ── Edge cases ─────────────────────────────────────────────────────────── + + def test_empty_mcp_servers_does_not_break(self) -> None: + """Sync with no MCP servers (empty mcp/ dir) exits gracefully without crash.""" + # Remove the MCP server we wrote in setUp + mcp_dir = self.root / "env" / "mcp" + for f in mcp_dir.glob("*.json"): + f.unlink() + + # main() returns early when mcp_all is empty — no crash, no settings.json + target_cfg = self.platform_cfg + self._write_json(self.root / "env" / "platforms" / "gemini.json", target_cfg) + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "gemini"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + + # main() returned early — no crash is the pass condition + self.assertTrue(True) + + def test_settings_json_not_found_graceful(self) -> None: + """When ~/.gemini/settings.json doesn't exist, sync creates it.""" + settings = self._run_gemini_sync() + + self.assertIn("model", settings) + self.assertIn("mcpServers", settings) + + def test_no_export_env_to_zshrc_skips_zshrc(self) -> None: + """When export_env_to_zshrc is absent/empty, zshrc is not touched.""" + cfg = dict(self.platform_cfg) + del cfg["export_env_to_zshrc"] + + self._run_gemini_sync(cfg) + + zshrc = self.root / "home" / ".zshrc" + self.assertFalse(zshrc.exists(), "zshrc should not be created without export_env_to_zshrc") + + +if __name__ == "__main__": + unittest.main() From de636c9a03cf366d34a114b4f89753f013c79b31 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 16:45:15 +0800 Subject: [PATCH 12/30] fix(gemini): update fileName and sandbox settings in configuration - Changed `fileName` in `gemini.json` from an array to a single string value for clarity. - Updated `sandbox` setting from `workspace-write` to `sandbox-exec` to enhance security and execution context. --- env/platforms/gemini.json | 4 ++-- tests/test_gemini_sync.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/env/platforms/gemini.json b/env/platforms/gemini.json index ecfccf6..f0e1a3c 100644 --- a/env/platforms/gemini.json +++ b/env/platforms/gemini.json @@ -7,7 +7,7 @@ "skipNextSpeakerCheck": true }, "context": { - "fileName": ["GEMINI.md", "CODEBUDDY.md", "CLAUDE.md"], + "fileName": "GEMINI.md", "includeDirectoryTree": true, "importFormat": "tree", "fileFiltering": { @@ -19,7 +19,7 @@ } }, "tools": { - "sandbox": "workspace-write", + "sandbox": "sandbox-exec", "sandboxNetworkAccess": true, "useRipgrep": true, "shell": { diff --git a/tests/test_gemini_sync.py b/tests/test_gemini_sync.py index 6ef6b26..4773403 100644 --- a/tests/test_gemini_sync.py +++ b/tests/test_gemini_sync.py @@ -122,11 +122,11 @@ def test_settings_json_contains_managed_keys(self) -> None: }, "settings", ) - self.assertEqual(settings["context"]["fileName"], ["GEMINI.md", "CODEBUDDY.md", "CLAUDE.md"]) + self.assertEqual(settings["context"]["fileName"], "GEMINI.md") self.assertTrue(settings["context"]["includeDirectoryTree"]) self.assertEqual(settings["context"]["importFormat"], "tree") self.assertTrue(settings["tools"]["useRipgrep"]) - self.assertEqual(settings["tools"]["sandbox"], "workspace-write") + self.assertEqual(settings["tools"]["sandbox"], "sandbox-exec") self.assertTrue(settings["tools"]["sandboxNetworkAccess"]) self.assertTrue(settings["tools"]["shell"]["enableInteractiveShell"]) self.assertTrue(settings["skills"]["enabled"]) From 9c9a87fcbb622ecfa8e5949e99208a97a48455d5 Mon Sep 17 00:00:00 2001 From: stack Date: Fri, 3 Jul 2026 17:11:16 +0800 Subject: [PATCH 13/30] feat(sync): improve MCP server handling and enhance model synchronization - Updated the message for no MCP servers found to indicate continuation with an empty config instead of exiting. - Added validation functions for model entries and available models in codebuddy.py to ensure correct data types and structures. - Implemented merging logic for model entries and available models, preserving user-added entries while prioritizing config-managed ones. - Enhanced the skill synchronization process to handle temporary and backup directories, ensuring safer updates. --- sync/platforms/codebuddy.py | 125 +++++++++- sync/platforms/common.py | 4 +- sync/sync_config.py | 3 +- tests/test_codebuddy_sync.py | 470 +++++++++++++++++++++++++++++++++++ 4 files changed, 593 insertions(+), 9 deletions(-) create mode 100644 tests/test_codebuddy_sync.py diff --git a/sync/platforms/codebuddy.py b/sync/platforms/codebuddy.py index 6832f66..3882da6 100644 --- a/sync/platforms/codebuddy.py +++ b/sync/platforms/codebuddy.py @@ -10,6 +10,89 @@ CLAUDE_SKILLS_DIR = Path.home() / ".claude" / "skills" +def _validate_model_entries(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + raise ValueError("platforms.codebuddy.models must be a list.") + + for index, entry in enumerate(value): + if not isinstance(entry, dict): + raise ValueError(f"platforms.codebuddy.models[{index}] must be an object.") + model_id = entry.get("id") + if not isinstance(model_id, str) or not model_id: + raise ValueError(f"platforms.codebuddy.models[{index}].id must be a non-empty string.") + return value + + +def _validate_available_models(value: Any) -> list[str]: + if not isinstance(value, list): + raise ValueError("platforms.codebuddy.availableModels must be a list.") + + for index, model_id in enumerate(value): + if not isinstance(model_id, str) or not model_id: + raise ValueError( + f"platforms.codebuddy.availableModels[{index}] must be a non-empty string." + ) + return value + + +def _merge_model_entries( + existing_entries: list[Any], config_entries: list[dict[str, Any]] +) -> list[Any]: + """Merge config-managed model entries into existing entries by id. + + - Config-managed entries (identified by ``id``) appear first in config order. + - Existing entries with the same id are silently updated (config wins). + - User-added entries not in config are preserved after config entries. + - Non-dict entries with no id are preserved at the very end. + """ + if not config_entries: + return existing_entries + + config_by_id: dict[str, dict[str, Any]] = {} + for m in config_entries: + if isinstance(m, dict) and "id" in m: + config_by_id[m["id"]] = m + + result: list[Any] = [] + + # Config-managed entries first (in config order) + for m in config_entries: + if isinstance(m, dict) and "id" in m: + result.append(m) + + # User-added entries from existing (not in config), preserving order + for m in existing_entries: + if not isinstance(m, dict) or "id" not in m: + continue + mid = m["id"] + if mid not in config_by_id: + result.append(m) + + # Trailing non-standard entries + for m in existing_entries: + if not isinstance(m, dict) or "id" not in m: + result.append(m) + + return result + + +def _merge_available_models( + existing_available: list[Any], config_available: list[Any] +) -> list[Any]: + """Merge config-managed availableModels with user-added entries. + + - Config-managed IDs appear first and replace any existing duplicates. + - User-added IDs not in the config are preserved after config entries. + """ + if not config_available: + return existing_available + + config_ids = set(config_available) + # User-added IDs not managed by our config + user_ids = [m for m in existing_available if m not in config_ids] + return list(config_available) + user_ids + + def _sync_models(cfg: dict[str, Any]) -> None: models = cfg.get("models") available_models = cfg.get("availableModels") @@ -19,12 +102,24 @@ def _sync_models(cfg: dict[str, Any]) -> None: return existing = read_json_object(MODELS_TARGET) + if models is not None: - existing["models"] = models + models = _validate_model_entries(models) + existing_entries = existing.get("models") + if not isinstance(existing_entries, list): + existing_entries = [] + existing["models"] = _merge_model_entries(existing_entries, models) + if available_models is not None: - existing["availableModels"] = available_models + available_models = _validate_available_models(available_models) + existing_avail = existing.get("availableModels") + if not isinstance(existing_avail, list): + existing_avail = [] + existing["availableModels"] = _merge_available_models(existing_avail, available_models) + write_json(MODELS_TARGET, existing) - print(f"Replaced models in {MODELS_TARGET}.") + print(f"Merged models into {MODELS_TARGET}.") + def _sync_skills() -> None: @@ -40,9 +135,29 @@ def _sync_skills() -> None: continue dest = CODEBUDDY_SKILLS_DIR / skill_dir.name + tmp = CODEBUDDY_SKILLS_DIR / f".{skill_dir.name}.tmp-sync" + backup = CODEBUDDY_SKILLS_DIR / f".{skill_dir.name}.backup-sync" + + if tmp.exists(): + shutil.rmtree(tmp) + if backup.exists(): + shutil.rmtree(backup) + + shutil.copytree(skill_dir, tmp) + if dest.exists(): - shutil.rmtree(dest) - shutil.copytree(skill_dir, dest) + dest.rename(backup) + try: + tmp.rename(dest) + except OSError: + if backup.exists() and not dest.exists(): + backup.rename(dest) + raise + finally: + if tmp.exists(): + shutil.rmtree(tmp) + if backup.exists(): + shutil.rmtree(backup) synced.append(skill_dir.name) print(f"Synced {len(synced)} skills to {CODEBUDDY_SKILLS_DIR}: {', '.join(synced) or '(none)'}.") diff --git a/sync/platforms/common.py b/sync/platforms/common.py index 8adce4d..e53f1dc 100644 --- a/sync/platforms/common.py +++ b/sync/platforms/common.py @@ -3,7 +3,7 @@ import re import shlex from pathlib import Path -from typing import Any +from typing import Any, Optional REPO_ROOT = Path(__file__).resolve().parents[2] MCP_DIR = REPO_ROOT / "env" / "mcp" @@ -337,7 +337,7 @@ def toml_inline_table(values: dict[str, Any]) -> str: return "{ " + ", ".join(f"{k} = {toml_value(v)}" for k, v in values.items()) + " }" -def toml_section(entries: dict[str, Any], *, ignore: set[str] | None = None) -> str: +def toml_section(entries: dict[str, Any], *, ignore: Optional[set[str]] = None) -> str: """Convert a dict tree to TOML key-value lines and [table] sections. Skips keys in `ignore` (default: {'env', '_comment', 'projects', 'model_providers'}). diff --git a/sync/sync_config.py b/sync/sync_config.py index f12084b..f4f8bdd 100644 --- a/sync/sync_config.py +++ b/sync/sync_config.py @@ -95,8 +95,7 @@ def _auto_export_env_to_zshrc(platform: str, platform_cfg: dict[str, Any]) -> No def main() -> None: mcp_all = load_all_mcp() if not mcp_all: - print("[sync] No MCP servers found in env/mcp/ — nothing to sync.") - return + print("[sync] No MCP servers found in env/mcp/ — continuing with empty MCP config.") all_targets = _auto_discover_targets() if not all_targets: diff --git a/tests/test_codebuddy_sync.py b/tests/test_codebuddy_sync.py new file mode 100644 index 0000000..880f5ea --- /dev/null +++ b/tests/test_codebuddy_sync.py @@ -0,0 +1,470 @@ +import contextlib +import io +import json +import os +import sys +import tempfile +import unittest +from collections.abc import Mapping +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SYNC_DIR = REPO_ROOT / "sync" +if str(SYNC_DIR) not in sys.path: + sys.path.insert(0, str(SYNC_DIR)) + +import sync_config # noqa: E402 +from platforms import codebuddy as codebuddy_mod # noqa: E402 +from platforms import common # noqa: E402 + + +@contextlib.contextmanager +def patched_sync_environment(root: Path): + """Redirect HOME and module-level paths for isolated CodeBuddy sync tests. + + codebuddy.py uses module-level constants (evaluated at import time via + Path.home()) whereas other sync modules use runtime functions. This + context manager patches those constants so all writes land in the + isolated test directory. + """ + home = root / "home" + old_env = {k: os.environ.get(k) for k in ("HOME",)} + old_paths = (common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH) + old_cb_paths = ( + codebuddy_mod.MCP_TARGET, + codebuddy_mod.MODELS_TARGET, + codebuddy_mod.CODEBUDDY_SKILLS_DIR, + codebuddy_mod.CLAUDE_SKILLS_DIR, + ) + old_argv = sys.argv[:] + try: + os.environ["HOME"] = str(home) + common.MCP_DIR = root / "env" / "mcp" + common.PLATFORMS_DIR = root / "env" / "platforms" + common.SECRETS_PATH = root / "env" / "secrets.json" + codebuddy_mod.MCP_TARGET = home / ".codebuddy" / "mcp.json" + codebuddy_mod.MODELS_TARGET = home / ".codebuddy" / "models.json" + codebuddy_mod.CODEBUDDY_SKILLS_DIR = home / ".codebuddy" / "skills" + codebuddy_mod.CLAUDE_SKILLS_DIR = home / ".claude" / "skills" + yield + finally: + for key, value in old_env.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + common.MCP_DIR, common.PLATFORMS_DIR, common.SECRETS_PATH = old_paths + (codebuddy_mod.MCP_TARGET, codebuddy_mod.MODELS_TARGET, + codebuddy_mod.CODEBUDDY_SKILLS_DIR, codebuddy_mod.CLAUDE_SKILLS_DIR) = old_cb_paths + sys.argv = old_argv + + +class CodeBuddySyncTests(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + self.platform_cfg = json.loads( + (REPO_ROOT / "env" / "platforms" / "codebuddy.json").read_text() + ) + self._write_json( + self.root / "env" / "mcp" / "sample.json", + { + "name": "sample", + "type": "stdio", + "command": "echo", + "args": ["hello"], + "platforms": ["codebuddy"], + }, + ) + self._write_json( + self.root / "env" / "secrets.json", + {"codebuddy": {"url": "https://codebuddy.example/v1", "key": "sk-test-codebuddy"}}, + ) + + def tearDown(self) -> None: + self.tmp.cleanup() + + def _write_json(self, path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=4) + "\n", encoding="utf-8") + + def _read_json(self, path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + def _run_codebuddy_sync(self, cfg: dict | None = None) -> dict[str, dict]: + """Run CodeBuddy sync and return parsed {mcp, models} contents.""" + target_cfg = cfg if cfg is not None else self.platform_cfg + self._write_json(self.root / "env" / "platforms" / "codebuddy.json", target_cfg) + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "codebuddy"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + return { + "mcp": self._read_json(self.root / "home" / ".codebuddy" / "mcp.json"), + "models": self._read_json(self.root / "home" / ".codebuddy" / "models.json"), + } + + def assert_nested_equal(self, data: Mapping, expected: Mapping, path: str) -> None: + for key, expected_value in expected.items(): + current_path = f"{path}.{key}" + self.assertIn(key, data, current_path) + actual_value = data[key] + if isinstance(expected_value, Mapping): + self.assertIsInstance(actual_value, Mapping, current_path) + self.assert_nested_equal(actual_value, expected_value, current_path) + else: + self.assertEqual(actual_value, expected_value, current_path) + + # ── MCP servers ────────────────────────────────────────────────────────── + + def test_mcp_servers_synced_to_mcp_json(self) -> None: + result = self._run_codebuddy_sync() + + self.assertIn("mcpServers", result["mcp"]) + self.assertIn("sample", result["mcp"]["mcpServers"]) + self.assertEqual(result["mcp"]["mcpServers"]["sample"]["command"], "echo") + self.assertEqual(result["mcp"]["mcpServers"]["sample"]["args"], ["hello"]) + + def test_mcp_json_preserves_existing_user_keys(self) -> None: + """User-added keys outside mcpServers are preserved after sync.""" + mcp_path = self.root / "home" / ".codebuddy" / "mcp.json" + mcp_path.parent.mkdir(parents=True, exist_ok=True) + mcp_path.write_text( + json.dumps( + { + "meta": {"version": 1, "lastModified": "2025-01-01"}, + "mcpServers": {"customServer": {"command": "custom-cmd"}}, + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + result = self._run_codebuddy_sync() + + self.assertEqual(result["mcp"]["meta"]["version"], 1) + self.assertEqual(result["mcp"]["meta"]["lastModified"], "2025-01-01") + # Managed MCP servers overwrite mcpServers key + self.assertIn("sample", result["mcp"]["mcpServers"]) + self.assertNotIn("customServer", result["mcp"]["mcpServers"]) + + # ── Models sync ────────────────────────────────────────────────────────── + + def test_models_synced_to_models_json(self) -> None: + result = self._run_codebuddy_sync() + + self.assertIn("models", result["models"]) + self.assertEqual(len(result["models"]["models"]), 2) + # Model 0: deepseek-v4-pro (with relatedModels) + self.assertEqual(result["models"]["models"][0]["id"], "deepseek-v4-pro") + self.assertEqual(result["models"]["models"][0]["name"], "DeepSeek V4 Pro") + self.assertEqual(result["models"]["models"][0]["vendor"], "dataeyes") + self.assertEqual(result["models"]["models"][0]["url"], "https://codebuddy.example/v1") + self.assertEqual(result["models"]["models"][0]["apiKey"], "sk-test-codebuddy") + self.assertEqual(result["models"]["models"][0]["maxInputTokens"], 128000) + self.assertEqual(result["models"]["models"][0]["maxOutputTokens"], 8192) + self.assertTrue(result["models"]["models"][0]["supportsToolCall"]) + self.assertFalse(result["models"]["models"][0]["supportsImages"]) + self.assertEqual( + result["models"]["models"][0]["relatedModels"], + {"lite": "deepseek-v4-flash", "reasoning": "deepseek-v4-pro"}, + ) + # Model 1: deepseek-v4-flash (no relatedModels) + self.assertEqual(result["models"]["models"][1]["id"], "deepseek-v4-flash") + self.assertEqual(result["models"]["models"][1]["name"], "DeepSeek V4 Flash") + self.assertNotIn("relatedModels", result["models"]["models"][1]) + + def test_available_models_synced(self) -> None: + result = self._run_codebuddy_sync() + + self.assertIn("availableModels", result["models"]) + self.assertEqual( + result["models"]["availableModels"], + ["deepseek-v4-pro", "deepseek-v4-flash"], + ) + + def test_models_json_preserves_existing_user_keys(self) -> None: + """User-added top-level keys outside models/availableModels survive sync.""" + models_path = self.root / "home" / ".codebuddy" / "models.json" + models_path.parent.mkdir(parents=True, exist_ok=True) + models_path.write_text( + json.dumps( + { + "meta": {"version": 2, "description": "Custom config"}, + "uiPreference": "compact", + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + result = self._run_codebuddy_sync() + + # User's top-level keys preserved + self.assertEqual(result["models"]["meta"]["version"], 2) + self.assertEqual(result["models"]["uiPreference"], "compact") + + def test_user_added_models_preserved_during_sync(self) -> None: + """User-added model entries not in config are preserved alongside managed ones.""" + models_path = self.root / "home" / ".codebuddy" / "models.json" + models_path.parent.mkdir(parents=True, exist_ok=True) + models_path.write_text( + json.dumps( + { + "models": [ + { + "id": "custom-model", + "name": "Custom Model", + "vendor": "custom", + } + ], + "availableModels": ["custom-model"], + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + result = self._run_codebuddy_sync() + + # 2 config-managed + 1 user-added = 3 + self.assertEqual(len(result["models"]["models"]), 3) + model_ids = [m["id"] for m in result["models"]["models"]] + self.assertIn("custom-model", model_ids) + self.assertIn("deepseek-v4-pro", model_ids) + self.assertIn("deepseek-v4-flash", model_ids) + # Config-managed entries appear first (in config order) + self.assertEqual(model_ids[0], "deepseek-v4-pro") + self.assertEqual(model_ids[1], "deepseek-v4-flash") + self.assertEqual(model_ids[2], "custom-model") + # User's availableModels entry is preserved + self.assertIn("custom-model", result["models"]["availableModels"]) + + def test_config_models_update_existing_by_id(self) -> None: + """Config-managed models update existing entries with the same id instead of duplicating.""" + models_path = self.root / "home" / ".codebuddy" / "models.json" + models_path.parent.mkdir(parents=True, exist_ok=True) + models_path.write_text( + json.dumps( + { + "models": [ + { + "id": "deepseek-v4-pro", + "name": "OLD DeepSeek V4 Pro", + "vendor": "old-vendor", + "url": "https://old.example/v1", + "apiKey": "sk-old-key", + } + ], + "availableModels": [], + }, + indent=4, + ) + + "\n", + encoding="utf-8", + ) + + result = self._run_codebuddy_sync() + + # deepseek-v4-pro is UPDATED (not duplicated), deepseek-v4-flash is ADDED + self.assertEqual(len(result["models"]["models"]), 2) + model_ids = [m["id"] for m in result["models"]["models"]] + self.assertEqual(model_ids, ["deepseek-v4-pro", "deepseek-v4-flash"]) + # Verify the updated model uses config values + pro = result["models"]["models"][0] + self.assertEqual(pro["name"], "DeepSeek V4 Pro") + self.assertEqual(pro["url"], "https://codebuddy.example/v1") + self.assertEqual(pro["apiKey"], "sk-test-codebuddy") + + # ── Skills sync ────────────────────────────────────────────────────────── + + def test_skills_synced_from_claude_to_codebuddy(self) -> None: + claude_skills = self.root / "home" / ".claude" / "skills" + # Create a SKILL.md for a valid skill + skill_dir = claude_skills / "test-skill" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("# Test Skill\n", encoding="utf-8") + (skill_dir / "helper.py").write_text("# helper script\n", encoding="utf-8") + + self._run_codebuddy_sync() + + dest = self.root / "home" / ".codebuddy" / "skills" / "test-skill" + self.assertTrue(dest.exists(), "Skill directory was not synced to CodeBuddy") + self.assertTrue((dest / "SKILL.md").exists(), "SKILL.md was not synced") + self.assertTrue((dest / "helper.py").exists(), "helper.py was not synced") + self.assertEqual( + (dest / "SKILL.md").read_text(encoding="utf-8"), "# Test Skill\n" + ) + + def test_skills_skip_dirs_without_skill_md(self) -> None: + claude_skills = self.root / "home" / ".claude" / "skills" + (claude_skills / "no-skill-md").mkdir(parents=True) + (claude_skills / "no-skill-md" / "readme.md").write_text("no SKILL.md\n", encoding="utf-8") + + self._run_codebuddy_sync() + + dest = self.root / "home" / ".codebuddy" / "skills" / "no-skill-md" + self.assertFalse(dest.exists(), "Dir without SKILL.md should not be synced") + + def test_skills_missing_claude_dir_skips_gracefully(self) -> None: + """When ~/.claude/skills doesn't exist, skill sync is skipped without error.""" + # setUp doesn't create claude skills, so dir doesn't exist + result = self._run_codebuddy_sync() + + skills_dir = self.root / "home" / ".codebuddy" / "skills" + self.assertFalse(skills_dir.exists(), "skills dir should not be created when claude skills missing") + # MCP and models should still sync fine + self.assertIn("mcpServers", result["mcp"]) + self.assertIn("models", result["models"]) + + def test_skills_overwrites_existing_skill_dir(self) -> None: + """Existing CodeBuddy skill dir is removed and replaced with Claude version.""" + claude_skills = self.root / "home" / ".claude" / "skills" + (claude_skills / "test-skill").mkdir(parents=True) + (claude_skills / "test-skill" / "SKILL.md").write_text("Claude version\n", encoding="utf-8") + + cb_skills = self.root / "home" / ".codebuddy" / "skills" / "test-skill" + cb_skills.mkdir(parents=True) + (cb_skills / "SKILL.md").write_text("Old CodeBuddy version\n", encoding="utf-8") + (cb_skills / "stale-file.txt").write_text("should be removed\n", encoding="utf-8") + + self._run_codebuddy_sync() + + self.assertTrue((cb_skills / "SKILL.md").exists()) + self.assertEqual((cb_skills / "SKILL.md").read_text(encoding="utf-8"), "Claude version\n") + self.assertFalse((cb_skills / "stale-file.txt").exists(), "Stale files should be removed") + + def test_skills_copy_failure_preserves_existing_skill_dir(self) -> None: + """Existing CodeBuddy skill remains when copying the Claude version fails.""" + claude_skills = self.root / "home" / ".claude" / "skills" + (claude_skills / "test-skill").mkdir(parents=True) + (claude_skills / "test-skill" / "SKILL.md").write_text("Claude version\n", encoding="utf-8") + + cb_skills = self.root / "home" / ".codebuddy" / "skills" / "test-skill" + cb_skills.mkdir(parents=True) + (cb_skills / "SKILL.md").write_text("Old CodeBuddy version\n", encoding="utf-8") + + original_copytree = codebuddy_mod.shutil.copytree + + def failing_copytree(src: Path, dst: Path): + raise OSError("simulated copy failure") + + codebuddy_mod.shutil.copytree = failing_copytree + try: + with self.assertRaises(OSError): + self._run_codebuddy_sync() + finally: + codebuddy_mod.shutil.copytree = original_copytree + + self.assertTrue((cb_skills / "SKILL.md").exists()) + self.assertEqual((cb_skills / "SKILL.md").read_text(encoding="utf-8"), "Old CodeBuddy version\n") + + # ── Edge cases ──────────────────────────────────────────────────────────── + + def test_no_models_config_skips_model_sync(self) -> None: + """When config has no 'models' or 'availableModels', model sync is skipped.""" + cfg: dict = {} + result = self._run_codebuddy_sync(cfg) + + # MCP should still work + self.assertIn("mcpServers", result["mcp"]) + # Models target should be empty (not created with stale data) + models_path = self.root / "home" / ".codebuddy" / "models.json" + self.assertFalse(models_path.exists(), "models.json should not be created without model config") + + def test_models_only_sync(self) -> None: + """When config has only 'models' but no 'availableModels', only models are synced.""" + cfg: dict = {"models": self.platform_cfg["models"]} + self._write_json(self.root / "env" / "platforms" / "codebuddy.json", cfg) + + result = self._run_codebuddy_sync(cfg) + + self.assertIn("models", result["models"]) + self.assertEqual(len(result["models"]["models"]), 2) + self.assertNotIn("availableModels", result["models"]) + + def test_available_models_only_sync(self) -> None: + """When config has only 'availableModels' but no 'models', only availableModels are synced.""" + cfg: dict = {"availableModels": self.platform_cfg["availableModels"]} + self._write_json(self.root / "env" / "platforms" / "codebuddy.json", cfg) + + result = self._run_codebuddy_sync(cfg) + + self.assertEqual( + result["models"]["availableModels"], + ["deepseek-v4-pro", "deepseek-v4-flash"], + ) + self.assertNotIn("models", result["models"]) + + def test_empty_mcp_servers_does_not_break(self) -> None: + """Sync with no MCP servers still runs CodeBuddy models sync.""" + mcp_dir = self.root / "env" / "mcp" + for f in mcp_dir.glob("*.json"): + f.unlink() + + target_cfg = self.platform_cfg + self._write_json(self.root / "env" / "platforms" / "codebuddy.json", target_cfg) + with patched_sync_environment(self.root): + sys.argv = ["sync_config.py", "--target", "codebuddy"] + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + sync_config.main() + + mcp = self._read_json(self.root / "home" / ".codebuddy" / "mcp.json") + models = self._read_json(self.root / "home" / ".codebuddy" / "models.json") + self.assertEqual(mcp["mcpServers"], {}) + self.assertEqual(len(models["models"]), 2) + + def test_mcp_json_not_found_creates_new_file(self) -> None: + """When ~/.codebuddy/mcp.json doesn't exist, sync creates it.""" + result = self._run_codebuddy_sync() + + self.assertIn("mcpServers", result["mcp"]) + self.assertIn("sample", result["mcp"]["mcpServers"]) + + def test_models_json_not_found_creates_new_file(self) -> None: + """When ~/.codebuddy/models.json doesn't exist, sync creates it.""" + result = self._run_codebuddy_sync() + + self.assertIn("models", result["models"]) + self.assertEqual(len(result["models"]["models"]), 2) + + def test_secret_resolution_in_models(self) -> None: + """Secrets ${codebuddy.url} and ${codebuddy.key} are resolved in model fields.""" + result = self._run_codebuddy_sync() + + model = result["models"]["models"][0] + self.assertEqual(model["url"], "https://codebuddy.example/v1") + self.assertEqual(model["apiKey"], "sk-test-codebuddy") + + def test_invalid_models_config_fails_fast(self) -> None: + """Invalid models config is rejected before writing models.json.""" + with self.assertRaisesRegex(ValueError, "platforms.codebuddy.models must be a list"): + self._run_codebuddy_sync({"models": {"id": "bad"}}) + + def test_invalid_available_models_config_fails_fast(self) -> None: + """Invalid availableModels config is rejected before writing models.json.""" + with self.assertRaisesRegex( + ValueError, + r"platforms\.codebuddy\.availableModels\[0\] must be a non-empty string", + ): + self._run_codebuddy_sync({"availableModels": [123]}) + + # ── Internal key exclusion ────────────────────────────────────────────── + + def test_internal_keys_excluded_from_output(self) -> None: + """_comment and platform-internal keys do not leak into output files.""" + result = self._run_codebuddy_sync() + + for key in ("_comment",): + self.assertNotIn(key, result["mcp"], f"Internal key '{key}' leaked into mcp.json") + self.assertNotIn(key, result["models"], f"Internal key '{key}' leaked into models.json") + + +if __name__ == "__main__": + unittest.main() From 23d891dcfd028a687f1f0b5950e8744f04aa4060 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 16:23:03 +0800 Subject: [PATCH 14/30] feat(skills-engineering): align with mattpocock/skills structure & fix gaps - Add AGENT-BRIEF.md for all 6 skills (Agent quick decision reference) - Add OUT-OF-SCOPE.md for all 6 skills (scope boundary declaration) - Add .claude-plugin/plugin.json (Claude Code plugin support) - Add .agents/ invocation & writing docs - Add .out-of-scope/ repository-level scope declaration - Add docs/ per-skill usage documentation (6 files) - Add CONTEXT.md, CHANGELOG.md, COMPARISON-REPORT.md - Add list-skills.sh script - Fix verify-sync.sh: add epistemic-integrity & problem-analysis preamble checks - Create missing epistemic-integrity.mdc.tmpl template --- .gitignore | 2 +- skills-engineering/.agents/README.md | 13 ++ skills-engineering/.agents/invocation.md | 41 +++++ skills-engineering/.agents/writing-docs.md | 69 +++++++++ skills-engineering/.claude-plugin/plugin.json | 24 +++ .../.out-of-scope/repository-scope.md | 28 ++++ skills-engineering/CHANGELOG.md | 19 +++ skills-engineering/COMPARISON-REPORT.md | 143 ++++++++++++++++++ skills-engineering/CONTEXT.md | 59 ++++++++ skills-engineering/README.md | 62 ++++++-- .../cognitive-expansion/AGENT-BRIEF.md | 24 +++ .../cognitive-expansion/OUT-OF-SCOPE.md | 13 ++ .../docs/cognitive-expansion.md | 34 +++++ .../docs/engineering-discipline.md | 33 ++++ .../docs/epistemic-integrity.md | 42 +++++ skills-engineering/docs/ios-engineer.md | 78 ++++++++++ skills-engineering/docs/logical-reasoning.md | 47 ++++++ skills-engineering/docs/problem-analysis.md | 39 +++++ .../engineering-discipline/AGENT-BRIEF.md | 25 +++ .../engineering-discipline/OUT-OF-SCOPE.md | 17 +++ .../epistemic-integrity/AGENT-BRIEF.md | 24 +++ .../epistemic-integrity/OUT-OF-SCOPE.md | 17 +++ .../ios-engineer/AGENT-BRIEF.md | 35 +++++ .../ios-engineer/OUT-OF-SCOPE.md | 27 ++++ .../logical-reasoning/AGENT-BRIEF.md | 26 ++++ .../logical-reasoning/OUT-OF-SCOPE.md | 20 +++ .../problem-analysis/AGENT-BRIEF.md | 22 +++ .../problem-analysis/OUT-OF-SCOPE.md | 22 +++ skills-engineering/scripts/list-skills.sh | 64 ++++++++ skills-engineering/scripts/sync-skills.sh | 2 + .../templates/epistemic-integrity.mdc.tmpl | 6 + skills-engineering/scripts/verify-sync.sh | 10 +- 32 files changed, 1075 insertions(+), 12 deletions(-) create mode 100644 skills-engineering/.agents/README.md create mode 100644 skills-engineering/.agents/invocation.md create mode 100644 skills-engineering/.agents/writing-docs.md create mode 100644 skills-engineering/.claude-plugin/plugin.json create mode 100644 skills-engineering/.out-of-scope/repository-scope.md create mode 100644 skills-engineering/CHANGELOG.md create mode 100644 skills-engineering/COMPARISON-REPORT.md create mode 100644 skills-engineering/CONTEXT.md create mode 100644 skills-engineering/cognitive-expansion/AGENT-BRIEF.md create mode 100644 skills-engineering/cognitive-expansion/OUT-OF-SCOPE.md create mode 100644 skills-engineering/docs/cognitive-expansion.md create mode 100644 skills-engineering/docs/engineering-discipline.md create mode 100644 skills-engineering/docs/epistemic-integrity.md create mode 100644 skills-engineering/docs/ios-engineer.md create mode 100644 skills-engineering/docs/logical-reasoning.md create mode 100644 skills-engineering/docs/problem-analysis.md create mode 100644 skills-engineering/engineering-discipline/AGENT-BRIEF.md create mode 100644 skills-engineering/engineering-discipline/OUT-OF-SCOPE.md create mode 100644 skills-engineering/epistemic-integrity/AGENT-BRIEF.md create mode 100644 skills-engineering/epistemic-integrity/OUT-OF-SCOPE.md create mode 100644 skills-engineering/ios-engineer/AGENT-BRIEF.md create mode 100644 skills-engineering/ios-engineer/OUT-OF-SCOPE.md create mode 100644 skills-engineering/logical-reasoning/AGENT-BRIEF.md create mode 100644 skills-engineering/logical-reasoning/OUT-OF-SCOPE.md create mode 100644 skills-engineering/problem-analysis/AGENT-BRIEF.md create mode 100644 skills-engineering/problem-analysis/OUT-OF-SCOPE.md create mode 100755 skills-engineering/scripts/list-skills.sh create mode 100644 skills-engineering/scripts/templates/epistemic-integrity.mdc.tmpl diff --git a/.gitignore b/.gitignore index dc83eff..6c42038 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,5 @@ env/secrets.json .cursor/ .codex/ .claude/ -docs/ +/docs/ skills-engineering/ios-engineer/evolution/usage diff --git a/skills-engineering/.agents/README.md b/skills-engineering/.agents/README.md new file mode 100644 index 0000000..cafb6ee --- /dev/null +++ b/skills-engineering/.agents/README.md @@ -0,0 +1,13 @@ +# Agent 调用指南 + +本目录包含 Agent 调用相关的全局指令。 + +## 文件说明 + +- `invocation.md`:Agent 调用规范——如何正确加载和执行 skill +- `writing-docs.md`:为 skills-engineering 贡献文档的写作规范 + +## Skill 加载优先级 + +各 skill 目录下的 `AGENT-BRIEF.md` 提供快速决策参考,`SKILL.md` 提供完整执行细则。 +Agent 应优先读 `AGENT-BRIEF.md` 判断是否加载该 skill,确认命中后完整读取 `SKILL.md` 和对应 `references/` 文件。 diff --git a/skills-engineering/.agents/invocation.md b/skills-engineering/.agents/invocation.md new file mode 100644 index 0000000..90c39fb --- /dev/null +++ b/skills-engineering/.agents/invocation.md @@ -0,0 +1,41 @@ +# Agent 调用规范 + +## 调用流程 + +1. **接收任务** → 解析用户意图和关键词 +2. **Skill 匹配** → 遍历已注册 skill 的 `AGENT-BRIEF.md`,判断是否命中触发条件 +3. **加载 Skill** → 命中后依次完整读取: + - `/SKILL.md` — 主入口和核心规则 + - `/references/.md` — 按路由表加载相关细则 + - `/OUT-OF-SCOPE.md` — 确认问题在 skill 范围内 +4. **执行规则** → 严格按 skill 规则执行回答 +5. **记录审计** → iOS 工程任务完成后追加 `` 块 + +## 多 Skill 并行 + +多个 skill 同时命中时并行加载: +- `ios-engineer` + 全局 skills(engineering-discipline、cognitive-expansion 等)可同时生效 +- 全局 skills 提供正交约束层(输出结构、论证质量、真值接地) +- 平台 skills 提供领域知识和具体修法 + +## Skill 命名约定 + +| 类型 | 格式 | 示例 | +|------|------|------| +| 平台 skill | `-engineer` | `ios-engineer` | +| 全局技能 | `-` | `cognitive-expansion`, `engineering-discipline` | +| 引用文件 | `snake_case.md` | `cognitive_expansion.md`, `swift_concurrency.md` | + +## Agent 判定速查 + +遇到以下关键词时,对应 skill 应在 1 个 turn 内加载: + +| 关键词 | Skill | 优先级 | +|--------|-------|--------| +| iOS / Swift / SwiftUI / Xcode / CocoaPods | ios-engineer | P0 | +| 卡顿 / 崩溃 / 内存泄漏 / 布局错位 | ios-engineer | P0 | +| 校准 / 真实 / 不确定 / 核验路径 | epistemic-integrity | P1 | +| 逻辑 / 推断 / 因果 / 论证 | logical-reasoning | P1 | +| 根因 / 修复 / 安全 / 敏感信息 | engineering-discipline | P1 | +| 第一性原理 / 深层需求 / 问题偏差 | problem-analysis | P1 | +| 盲区 / 邻域 / 拓展 / 带走 | cognitive-expansion | P2(回答后追加) | diff --git a/skills-engineering/.agents/writing-docs.md b/skills-engineering/.agents/writing-docs.md new file mode 100644 index 0000000..0ad2767 --- /dev/null +++ b/skills-engineering/.agents/writing-docs.md @@ -0,0 +1,69 @@ +# 文档写作规范 + +## 文件命名 + +| 文件 | 命名规则 | 示例 | +|------|---------|------| +| Skill 入口 | `SKILL.md` | `ios-engineer/SKILL.md` | +| 参考细则 | `snake_case.md` | `references/swift_concurrency.md` | +| Agent 简报 | `AGENT-BRIEF.md` | `AGENT-BRIEF.md` | +| 范围外声明 | `OUT-OF-SCOPE.md` | `OUT-OF-SCOPE.md` | +| 规则索引 | `rule_index.md` | `references/rule_index.md` | + +## 规则 ID 规范 + +| 前缀 | 含义 | 示例 | +|------|------|------| +| `IR-` | 核心铁律(Iron Rules) | IR-001, IR-006 | +| `GR-` | 全局规则(Global Rules) | GR-001, GR-010 | +| `ROUTE-` | 任务路由 | ROUTE-001 | +| `SYM-` | 症状映射 | SYM-001 | +| `OUT-` | 输出模板 | OUT-001 | +| `PA-` | 问题分析 | PA-001 | + +ID 必须在 `rule_index.md` 中注册并保持 `status=active`。 + +## 文档结构约定 + +### SKILL.md 结构 +```markdown +--- +name: +description: <一句话描述> +--- + +# + +## 强制入口 / 核心铁律 + +## 任务分流 / 路由表 + +## 输出模板 + +## 何时加载 / 跳过条件 +``` + +### References 结构 +```markdown +# <主题> + +## 规则声明 +[规则ID] <规则描述> + +## 细则 +... +``` + +## 链接约定 + +- Skill 内引用 reference:相对路径 `references/.md` +- 跨 skill 引用:`..//references/.md` +- 规则 ID 引用:`[ID]` 格式内联 + +## 变更约定 + +对规则文件的任何变更必须通过受控演进流程: +1. 创建 proposal(`create_skill_proposal.sh`) +2. 运行基础校验(`validate_skill_evolution.sh`) +3. 记录验证与审批 +4. 运行晋升(`promote_skill_evolution.sh`) diff --git a/skills-engineering/.claude-plugin/plugin.json b/skills-engineering/.claude-plugin/plugin.json new file mode 100644 index 0000000..22edeb6 --- /dev/null +++ b/skills-engineering/.claude-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "skills-engineering", + "version": "3.0.0", + "description": "iOS Engineering Skills for Claude Code — architecture, concurrency, networking, performance, crash debugging, code review, and more.", + "author": { + "name": "skills-engineering" + }, + "repository": "https://github.com/i-stack/ai-coding-kit", + "homepage": "https://github.com/i-stack/ai-coding-kit/tree/main/skills-engineering", + "skills": "./", + "keywords": [ + "ios", + "swift", + "swiftui", + "uikit", + "xcode", + "engineering", + "claude-code", + "codex", + "cursor", + "skills" + ], + "license": "MIT" +} diff --git a/skills-engineering/.out-of-scope/repository-scope.md b/skills-engineering/.out-of-scope/repository-scope.md new file mode 100644 index 0000000..c99f8b4 --- /dev/null +++ b/skills-engineering/.out-of-scope/repository-scope.md @@ -0,0 +1,28 @@ +# 仓库级范围外声明 + +本文件定义跨 skill 通用的范围约束,作为各 skill `OUT-OF-SCOPE.md` 的补充。 + +## 通用不处理内容 + +### 1. 主流问题追踪器限定 + +本仓库 skills 针对的开发和交互场景基于主流问题追踪器(GitHub Issues、Linear、Jira 等)。非标准或自定义追踪器的问题类型不在覆盖范围内。 + +### 2. 问题数量限制 + +单次交互中提出的问题若超过合理的上下文承载能力(通常建议 ≤3 个独立问题),skills 可能在分流和路由时产生交叉干扰。建议将大规模任务拆分为多次独立交互。 + +### 3. Skill 验证模式 + +当用户要求验证或测试 skill 行为时,skills 可能进入验证模式,此时部分守卫规则(如前置确认、最少 ref 加载等)可能被放宽以允许观察全量行为。验证完成后应恢复正常模式。 + +## 安全与合规 + +所有 skills 严格遵守: +- 不读取、不打印、不提交敏感机密(.env、密钥、证书、API Token) +- 不执行未经用户确认的高风险 shell 命令 +- 不绕过平台的安全限制 + +## 语言 + +默认使用简体中文进行交互和输出。 diff --git a/skills-engineering/CHANGELOG.md b/skills-engineering/CHANGELOG.md new file mode 100644 index 0000000..6045c03 --- /dev/null +++ b/skills-engineering/CHANGELOG.md @@ -0,0 +1,19 @@ +# skills-engineering Changelog + +## [Unreleased] + +### Added (2026-07-05) +- **AGENT-BRIEF.md**: 为 cognitive-expansion、engineering-discipline、epistemic-integrity、ios-engineer、logical-reasoning、problem-analysis 六个 skill 添加 Agent 快速决策参考 +- **OUT-OF-SCOPE.md**: 为所有六个 skill 添加职责边界声明,明确不处理的内容 +- **.claude-plugin/plugin.json**: Claude Code 插件清单,支持一键安装 +- **.agents/**: Agent 调用规范与文档写作规范 +- **.out-of-scope/**: 仓库级范围外声明 +- **docs/**: 每个 skill 的独立使用文档 +- **CONTEXT.md**: 仓库用途与快速上手指南 +- **CHANGELOG.md**: 仓库变更日志(本文件) +- **COMPARISON-REPORT.md**: 与 mattpocock/skills 开源库的深度对比分析报告 +- **list-skills.sh**: 列出所有已注册 skill 及描述 + +### Fixed +- `verify-sync.sh`: 补全 epistemic-integrity 和 problem-analysis 的 preamble 检查 +- 创建缺失的 `epistemic-integrity.mdc.tmpl` 模板,补齐 Cursor 生成链路 diff --git a/skills-engineering/COMPARISON-REPORT.md b/skills-engineering/COMPARISON-REPORT.md new file mode 100644 index 0000000..d5f73fe --- /dev/null +++ b/skills-engineering/COMPARISON-REPORT.md @@ -0,0 +1,143 @@ +# skills-engineering vs mattpocock/skills 深度对比分析报告 + +> 分析日期:2026-07-05 + +--- + +## 一、mattpocock/skills 开源库功能分析 + +该仓库由 TypeScript 专家 Matt Pocock 维护,是一个面向 AI Agent(尤其是 Claude Code)的 **Skill 集合和发布工具链**。核心特点: + +| 维度 | 描述 | +|------|------| +| **Skill 结构** | `SKILL.md`(主文件)+ `AGENT-BRIEF.md`(Agent 速览)+ `OUT-OF-SCOPE.md`(范围外) | +| **分类体系** | `engineering/`、`productivity/`、`misc/`、`personal/`、`in-progress/`、`deprecated/` | +| **插件发布** | `.claude-plugin/plugin.json` — 可作为 Claude Code 插件一键安装 | +| **文档深度** | 每个 skill 有独立的 `docs/.md` | +| **Agent 治理** | `.agents/` 目录包含调用规范和文档写作规范 | +| **仓库范围** | `.out-of-scope/` 声明跨 skill 通用约束 | +| **工程化** | `package.json` + npm 发布 + changeset 版本管理 | +| **工具脚本** | `list-skills.sh`、`link-skills.sh` | + +### Skill 清单 + +#### engineering/ (工程类) +- `triage` — 快速分诊 bug 报告,不做深入修复 +- `code-review` — 代码审查,严格检查清单 +- `implement` — 从 spec → implementation 全程 +- `tdd` — 测试驱动开发 +- `diagnosing-bugs` — 系统化 bug 定位 +- `research` — 技术调研与方案对比 +- `domain-modeling` — 领域建模 +- `improve-codebase-architecture` — 架构改进 +- `resolving-merge-conflicts` — 合并冲突解决 +- `to-prd` — 需求转 PRD + +#### productivity/ (生产力类) +- `handoff` — 工作交接记录,确保上下文不丢失 +- `grill-me` / `grilling` — 追问-反驳式审查 +- `teach` — 以教代学的解释模式 +- `writing-great-skills` — Skill 写作方法论(元 Skill) + +#### misc/ (杂项) +- `git-guardrails-claude-code` — Git 安全护栏 + +--- + +## 二、skills-engineering 的独特优势(mattpocock 没有的) + +skills-engineering 在以下方面**远超** mattpocock/skills: + +1. **受控演进流水线**(mattpocock 只用 changeset,无 governance) +2. **规则 ID 体系**(IR/SYM/ROUTE/OUT/GR/PA)及 `rule_index.md` 索引 +3. **多端自动同步**(Codex/Claude/Cursor/Gemini/Xcode 一键同步) +4. **Pre-commit/Pre-push 守卫**(规则变更必须绑定治理记录) +5. **Usage Ledger**(使用观测与效果评估) +6. **认知对手模式**(Step 0–6 全链条反迎合机制) +7. **ROUTE 精确定向**(症状 → 路由 → 仅加载 2–4 份 reference) +8. **12 类校验**(`validate_skill_evolution.sh` 伞形入口) + +--- + +## 三、发现的问题与修复 + +### 🐛 问题 1:`verify-sync.sh` preamble 检查不完整 + +`check_preamble_tilde()` 函数只检查了 cognitive-expansion、logical-reasoning、engineering-discipline 三个 skill 的 preamble 引用,漏掉了 epistemic-integrity 和 problem-analysis。 + +**修复**:已在 `verify-sync.sh` 第 89–94 行补全。 + +### 🐛 问题 2:`epistemic-integrity.mdc.tmpl` 模板缺失 + +sync-manifest 中注册了 `skill:epistemic-integrity`,但 `scripts/templates/` 下没有对应的 `.mdc.tmpl`。 + +**修复**:已创建该模板,补齐了 Cursor `.mdc` 生成链路。 + +--- + +## 四、从 mattpocock/skills 补全的新增功能 + +| 新增内容 | 说明 | +|---------|------| +| **各 skill 的 `AGENT-BRIEF.md`**(6 个) | Agent 快速决策参考:触发条件、关键行为、不调用情况 | +| **各 skill 的 `OUT-OF-SCOPE.md`**(6 个) | 明确声明 skill 不处理的内容,防止误触发 | +| **`.claude-plugin/plugin.json`** | Claude Code 插件清单,支持一键安装为 Claude 插件 | +| **`.agents/invocation.md`** | Agent 调用规范与多 skill 并行加载流程 | +| **`.agents/writing-docs.md`** | 文档写作规范(命名、ID 格式、结构约定) | +| **`.out-of-scope/repository-scope.md`** | 仓库级范围外声明(安全合规、问题数量限制等) | +| **`docs/*.md`**(6 个) | 每个 skill 的独立使用文档 | +| **`CONTEXT.md`** | 仓库用途与快速上手指南(给人类读) | +| **`CHANGELOG.md`** | 仓库变更日志(与 skill 内部的 evolution 历史互补) | +| **`list-skills.sh`** | 列出所有已注册 skill 及描述 | + +--- + +## 五、功能对比总览 + +| 功能 | mattpocock/skills | skills-engineering(修复前) | skills-engineering(修复后) | +|------|:---:|:---:|:---:| +| SKILL.md 主入口 | ✅ | ✅ | ✅ | +| AGENT-BRIEF.md | ✅ | ❌ | ✅ | +| OUT-OF-SCOPE.md | ✅ | ❌ | ✅ | +| .claude-plugin/plugin.json | ✅ | ❌ | ✅ | +| .agents/ 调用指南 | ✅ | ❌(仅有 openai.yaml) | ✅ | +| .out-of-scope/ 仓库约束 | ✅ | ❌ | ✅ | +| docs/ 使用文档 | ✅ | ❌ | ✅ | +| CONTEXT.md | ✅ | ❌ | ✅ | +| CHANGELOG.md | ✅ | ❌ | ✅ | +| list-skills.sh | ✅ | ❌ | ✅ | +| 受控演进 governance | ❌ | ✅ | ✅ | +| 规则 ID 体系 | ❌ | ✅ | ✅ | +| 多端自动同步 | ❌ | ✅ | ✅ | +| 认知对手模式 | ❌ | ✅ | ✅ | +| Usage Ledger | ❌ | ✅ | ✅ | +| Pre-commit/Pre-push 守卫 | ❌ | ✅ | ✅ | + +--- + +## 六、各自适合的使用场景 + +### 选择 mattpocock/skills 的场景 +- 需要轻量级、开箱即用的工程 skill 集合 +- 使用 TypeScript/Node.js 技术栈 +- 偏好 npm 生态和 changeset 版本管理 +- 需要快速集成到 Claude Code 插件体系 + +### 选择 skills-engineering 的场景 +- 需要跨平台 Agent 同步(Claude/Codex/Cursor/Gemini/Xcode) +- 需要可审计的规则变更治理 +- 需要 iOS/Swift 垂直领域的专业指导 +- 需要"认知对手"模式防止 AI 迎合用户 +- 需要精确的路由和按需加载机制(ROUTE 定向) +- 需要规则使用效果的可观测性(Usage Ledger) + +--- + +## 七、总结 + +两个仓库在设计哲学上有本质区别: + +- **mattpocock/skills** 是"轻量级技能市场"——注重可发现性、可安装性、社区贡献友好 +- **skills-engineering** 是"受控工程规则平台"——注重正确性保证、变更治理、跨平台一致性 + +修复后,skills-engineering 在 **工程规范性** 上已经基本对齐 mattpocock/skills 的仓库结构(补齐了 AGENT-BRIEF、OUT-OF-SCOPE、插件清单、文档等),同时保留了自身在 **治理、同步、路由、反迎合** 方面的核心差异化优势。 diff --git a/skills-engineering/CONTEXT.md b/skills-engineering/CONTEXT.md new file mode 100644 index 0000000..e779b5b --- /dev/null +++ b/skills-engineering/CONTEXT.md @@ -0,0 +1,59 @@ +# skills-engineering — 给人类的上下文 + +## 这是什么? + +`skills-engineering` 是一个**受控工程规则平台**,通过 Skill(技能)形式向 AI Agent(Claude Code、Codex、Cursor、Gemini、Xcode)提供领域工程指导和纪律约束。 + +它与 mattpocock/skills 不同——这里是"受控治理",不是"技能市场"。 + +## 快速开始 + +### 1. 查看所有 Skill + +```bash +bash scripts/list-skills.sh +``` + +### 2. 同步到本地 Agent + +```bash +bash scripts/sync-skills.sh +``` + +### 3. 验证同步完整性 + +```bash +bash scripts/verify-sync.sh +``` + +## Skill 分类 + +| Skill | 类型 | 说明 | +|-------|------|------| +| `ios-engineer` | 领域 | iOS/Swift/SwiftUI 工程全流程 | +| `engineering-discipline` | 纪律 | 安全合规、最小修复、防 Diff 噪声 | +| `epistemic-integrity` | 纪律 | 真值接地——不编造、不伪装确定 | +| `logical-reasoning` | 纪律 | 论证链可追溯、因果克制 | +| `problem-analysis` | 前置 | 问题分析——充分理解后再行动 | +| `cognitive-expansion` | 认知 | 打破知识茧房、邻域启发 | + +## 治理模型 + +- **规则 ID 体系**:IR/SYM/ROUTE/OUT/GR/PA 分类标识 +- **受控演进**:规则变更需绑定治理记录 +- **Usage Ledger**:使用观测与效果评估 +- **ROUTE 定向**:症状 → 路由 → 按需加载 + +## 关键文件 + +| 文件 | 用途 | +|------|------| +| `*/SKILL.md` | Skill 主入口 | +| `*/AGENT-BRIEF.md` | Agent 快速决策参考 | +| `*/OUT-OF-SCOPE.md` | 职责范围外声明 | +| `*/references/` | 详细规则文档 | +| `scripts/` | 同步/校验/工具脚本 | +| `docs/` | 使用文档 | +| `.claude-plugin/` | Claude Code 插件配置 | +| `.agents/` | Agent 调用规范 | +| `.out-of-scope/` | 仓库级约束 | diff --git a/skills-engineering/README.md b/skills-engineering/README.md index b49b19b..9b69bd1 100644 --- a/skills-engineering/README.md +++ b/skills-engineering/README.md @@ -4,7 +4,18 @@ ![Agent](https://img.shields.io/badge/agent-skill--engineering-34C759) ![Sync](https://img.shields.io/badge/sync-Codex%20%7C%20Claude%20%7C%20Cursor%20%7C%20Gemini-5856D6) -用于维护、同步与演进工程化 Agent Skill 的仓库。当前主技能为 `ios-engineer`,覆盖 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务中的架构、并发、网络、UI、性能、测试、审查、迁移和发布风险控制。 +用于维护、同步与演进工程化 Agent Skill 的仓库。 + +## 当前技能 + +| 技能 | 类型 | 描述 | +|------|------|------| +| `ios-engineer` | 平台技能 | iOS / Swift / SwiftUI / UIKit / Xcode 工程全生命周期 | +| `cognitive-expansion` | 全局技能 | 每次回复后的认知拓展,打破知识茧房 | +| `engineering-discipline` | 全局技能 | 工程纪律:安全合规、前置确认、四段式输出 | +| `epistemic-integrity` | 全局技能 | 真值接地:反幻觉、验证方法论、求真边界 | +| `logical-reasoning` | 全局技能 | 论证纪律:可追溯逻辑链、层级分明 | +| `problem-analysis` | 全局技能 | 问题前置分析:逻辑检验、第一性原理拆解 | 本仓库同时提供三类能力: @@ -30,22 +41,34 @@ ```text . ├── README.md -├── cognitive-expansion/ +├── ios-engineer/ # iOS 工程主技能 +│ ├── SKILL.md # 技能主入口 +│ ├── AGENT-BRIEF.md # Agent 快速决策参考 +│ ├── OUT-OF-SCOPE.md # 范围外声明 +│ ├── references/ # 28+ 参考细则文件 +│ ├── scripts/ # 演进治理脚本 +│ └── evolution/ # 变更历史与提案 +├── cognitive-expansion/ # 认知拓展技能 │ ├── SKILL.md +│ ├── AGENT-BRIEF.md +│ ├── OUT-OF-SCOPE.md │ └── references/ -├── scripts/ +├── engineering-discipline/ # 工程纪律技能(同构) +├── epistemic-integrity/ # 真值接地技能(同构) +├── logical-reasoning/ # 逻辑论证技能(同构) +├── problem-analysis/ # 问题分析技能(同构) +├── scripts/ # 仓库级脚本 │ ├── bootstrap.sh -│ ├── sync-agent-preamble.sh │ ├── sync-skills.sh +│ ├── sync-agent-preamble.sh │ ├── verify-sync.sh +│ ├── list-skills.sh │ ├── config.local.sh.example │ └── templates/ -└── ios-engineer/ - ├── SKILL.md - ├── agents/ - ├── references/ - ├── scripts/ - └── evolution/ +├── docs/ # 各 skill 使用文档(供人类阅读) +├── .agents/ # Agent 调用规范与文档写作规范 +├── .claude-plugin/ # Claude Code 插件清单(一键安装) +└── .out-of-scope/ # 仓库级范围外声明 ``` 关键目录: @@ -54,6 +77,10 @@ - `ios-engineer/scripts/`:技能演进、校验、提案、验证、晋升、回滚、usage ledger 写入与汇总脚本。 - `ios-engineer/evolution/`:技能演进数据,包括 `proposals/`、`validations/`、`approvals/`、`history/`、`scenarios/`、`usage/`。 - `scripts/`:仓库级脚本,负责同步技能、同步 Agent preamble 与同步结果校验;本地机器专属配置放在 `scripts/config.local.sh`(模板为 `scripts/config.local.sh.example`),路径由仓库根 `.gitignore` 排除,会被 sync 脚本自动 source。 +- `docs/`:各 skill 的独立使用文档,供人类阅读,不参与 Agent 运行时加载。 +- `.agents/`:`invocation.md`(多 skill 并行加载规范)和 `writing-docs.md`(文档写作规范)。 +- `.claude-plugin/plugin.json`:Claude Code 插件清单,支持一键安装为 Claude 插件。 +- `.out-of-scope/repository-scope.md`:仓库级范围外声明(安全合规等跨 skill 通用约束)。 - 提交/推送守卫:合并入 `ai-coding-kit` 后由仓库根的 [../.githooks/](../.githooks/) 统一管理,详见外层根 README 的「Git 钩子」章节。 ## 快速开始 @@ -394,3 +421,18 @@ git push --no-verify # 跳过整个 pre-push(含 sync/sync_all - 修改托管 preamble 时只改 `scripts/templates/agent-preamble.md.tmpl`,再运行 `./scripts/sync-agent-preamble.sh --dry-run` 检查输出。 - 推送前(或 `SKILL_BYPASS=1` 推送后)手动跑 `./scripts/verify-sync.sh` 确认各已启用缓存与 preamble 状态一致,避免 Agent 侧加载漂移版本。 - 本机专属配置(如 `CURSOR_PROJECT_ROOTS`)写进 `scripts/config.local.sh`(由 `scripts/config.local.sh.example` 复制);该路径在仓库根 `.gitignore` 中已排除,切勿提交进仓库。 + +## 变更记录 + +仓库结构与工具链的变化记录于此;各 skill 内部规则变化通过 `ios-engineer/evolution/` 管理。 + +### 3.0.0 — 2026-07-05 + +- 新增各 skill 目录的 `AGENT-BRIEF.md`(Agent 快速决策参考)和 `OUT-OF-SCOPE.md`(范围外声明) +- 新增 `docs/`:每个 skill 的独立使用文档 +- 新增 `.agents/`:`invocation.md` 和 `writing-docs.md` +- 新增 `.claude-plugin/plugin.json`:Claude Code 插件清单 +- 新增 `.out-of-scope/repository-scope.md`:仓库级范围外声明 +- 新增 `scripts/list-skills.sh`:列出所有已注册 skill 及描述 +- 新增 `scripts/templates/epistemic-integrity.mdc.tmpl`:补齐 Cursor `.mdc` 生成链路 +- 修复 `scripts/verify-sync.sh`:补齐 `epistemic-integrity` 和 `problem-analysis` 的 preamble 检查 diff --git a/skills-engineering/cognitive-expansion/AGENT-BRIEF.md b/skills-engineering/cognitive-expansion/AGENT-BRIEF.md new file mode 100644 index 0000000..e4dc143 --- /dev/null +++ b/skills-engineering/cognitive-expansion/AGENT-BRIEF.md @@ -0,0 +1,24 @@ +# cognitive-expansion Agent 调用指南 + +## 一句话描述 + +每次回复后的认知拓展(重框/盲区/邻域/带走),打破知识茧房;与 ios-engineer 认知对手模式互补。全局适用,不限于 iOS 工程。 + +## 何时调用 + +- **门控触发**(Tier 0):回答含真实判断/取舍/归因/设计选择,且能产出 ≥1 条可证伪盲区时追加认知尾注。 +- **用户主动**(Tier 3):用户输入 `【深潜】` / `【拓展】` 时加载深化模式。 +- **跳过**:用户明确「只要答案/不要延伸」;纯事实复述;门控未命中。 + +## 关键行为 + +1. 阅读 `SKILL.md` + `references/cognitive_expansion.md` 全文。 +2. 按 Tier 0 输出:重框(重新框定问题)、盲区(模型未知/不确定的点)、邻域(相邻领域对照)、带走(可操作的后续行动)。 +3. 不过度延伸、不堆砌——每条须可证伪、可行动。 +4. 与 `ios-engineer` 认知对手模式分工清晰:Tier 2 管反迎合/挑战,本 skill 管拓展。 + +## 不调用的情况 + +- 纯信息查询不含判断 +- 用户跳过认知拓展 +- Tier 0 触发条件不满足 diff --git a/skills-engineering/cognitive-expansion/OUT-OF-SCOPE.md b/skills-engineering/cognitive-expansion/OUT-OF-SCOPE.md new file mode 100644 index 0000000..867e26f --- /dev/null +++ b/skills-engineering/cognitive-expansion/OUT-OF-SCOPE.md @@ -0,0 +1,13 @@ +# cognitive-expansion 范围外 + +本 skill 负责**回复后的认知拓展**(打破知识茧房),不负责回答内容本身。 + +## 不处理的内容 + +- **回答的主体内容**:本 skill 在主体回答完成后追加认知尾注,不参与主体回答的生成。 +- **认知对手模式**:技术决策/架构取舍中的反迎合/挑战由 `ios-engineer/references/cognitive_adversary_mode.md` 负责(Tier 2),本 skill 管 Tier 0(尾注)和 Tier 3(深潜/拓展)。 +- **纯事实复述**:不含判断/取舍/归因/设计选择的纯信息查询不需要认知拓展。 + +## 触发门控 + +Tier 0 认知尾注**默认不触发**。仅当本次回答含真实判断/取舍/归因/设计选择,**且**能产出至少 1 条可证伪盲区时才追加。否则静默跳过。 diff --git a/skills-engineering/docs/cognitive-expansion.md b/skills-engineering/docs/cognitive-expansion.md new file mode 100644 index 0000000..82695f8 --- /dev/null +++ b/skills-engineering/docs/cognitive-expansion.md @@ -0,0 +1,34 @@ +# cognitive-expansion 使用文档 + +## 概述 + +`cognitive-expansion` 是全局认知拓展技能,在每次含真实判断的回答后追加认知尾注,打破 AI 的"知识茧房"效应。与 `ios-engineer` 的认知对手模式互补。 + +## 核心能力 + +### Tier 0:认知尾注(默认) +回答含真实判断/取舍/归因/设计选择时,自动追加: +- **重框**:重新框定问题的视角 +- **盲区**:模型未知或不确定的点 +- **邻域**:相邻领域的对照参考 +- **带走**:可操作的后续行动 + +### Tier 3:深潜/拓展(用户主动) +用户输入 `【深潜】` 或 `【拓展】` 时,加载深化模式进行更深入的认知拓展。 + +### 触发门控 +默认不触发。仅当本次回答含真实判断且能产出 ≥1 条可证伪盲区时追加。 + +## 与其他 Skill 的分工 + +| Tier | 职责 | 负责 Skill | +|------|------|-----------| +| Tier 0 | 认知尾注(重框/盲区/邻域/带走) | cognitive-expansion | +| Tier 2 | 认知对手(反迎合/挑战/red team) | ios-engineer (cognitive_adversary_mode) | +| Tier 3 | 深潜/拓展 | cognitive-expansion | + +## 跳过条件 + +- 用户明确「只要答案 / 不要延伸」 +- 纯事实复述不含判断 +- 触发门控未命中(无 ≥1 条可证伪盲区) diff --git a/skills-engineering/docs/engineering-discipline.md b/skills-engineering/docs/engineering-discipline.md new file mode 100644 index 0000000..8ea8536 --- /dev/null +++ b/skills-engineering/docs/engineering-discipline.md @@ -0,0 +1,33 @@ +# engineering-discipline 使用文档 + +## 概述 + +`engineering-discipline` 是全局工程纪律 skill,定义了所有工程类任务必须遵循的输出结构和行为准则(GR-001 至 GR-008)。它是正交于领域技能的约束层——无论处理什么平台/语言的工程问题,都应遵守这些纪律。 + +## 核心规则 + +| 规则 ID | 规则名称 | 说明 | +|---------|---------|------| +| GR-001 | 安全合规防御 | 绝不读取/打印/提交敏感机密;高风险操作前安全自检 | +| GR-002 | 前置确认 | 描述不清/歧义时先输出独立「前置确认」块 | +| GR-003 | 单根因 | 锁定 1 个最高概率根因,最多 1 个备选 | +| GR-004 | 四段式输出 | 根因 → 为什么 → 修法 → 验证 | +| GR-005 | 最小修复 | 先给最小可验证修复,不先提出整模块重写 | +| GR-006 | 预算拦截 | 连续失败 3 次或 turn 数超 15 时主动中断确认 | +| GR-007 | 防 Diff 噪声 | 不格式化代码(除非明确要求);自动修复限于 Staged 变更 | +| GR-008 | 残留风险声明 | 任何改动声明已覆盖/未覆盖/残留风险 | + +## 与平台 Skill 的关系 + +`engineering-discipline` 定义的是 **"如何输出"**(how),平台 skill(如 ios-engineer)定义的是 **"输出什么"**(what)。两者在工程任务中同时生效,互不冲突: +- ios-engineer 提供 iOS 领域知识和修法 +- engineering-discipline 约束输出格式和行为边界 + +## 加载方式 + +所有工程类任务默认加载。纯闲聊、无判断成分的机械执行时跳过。 + +``` +# Preamble 托管块中自动加载 +SKILL 规则位于 ~/.codex/skills/engineering-discipline +``` diff --git a/skills-engineering/docs/epistemic-integrity.md b/skills-engineering/docs/epistemic-integrity.md new file mode 100644 index 0000000..703e32a --- /dev/null +++ b/skills-engineering/docs/epistemic-integrity.md @@ -0,0 +1,42 @@ +# epistemic-integrity 使用文档 + +## 概述 + +`epistemic-integrity` 是全局真值接地 skill,确保 AI 输出与**外部真实世界**保持一致。它不负责答复内容的领域正确性(那是平台 skill 的职责),而是确保: +- 不确定的就说「不确定」 +- 关键事实有来源 +- 对方能廉价验证 + +## 核心规则 + +| 规则 ID | 规则名称 | 说明 | +|---------|---------|------| +| GR-011 | 反幻觉接地 | 高危带默认降置信,优先工具核验;关键事实给出「怎么核」的把手 | +| GR-012 | 验证方法论 | 现实>有问责一手源>独立交叉;优先证伪;按代价分级核验力度 | +| GR-013 | 求真方法边界 | 事实类查证不推导;校准把握度而非消除语气;冷静措辞不等于可信 | + +## 验证锚点块 + +高风险事实结论输出时,必须包含独立的「验证锚点」块: + +``` +## 验证锚点 +- **结论**:<断言> +- **依据来源**:<一手文档 / 可运行验证 / 独立来源> +- **置信度**:<高/中/低> +- **怎么核·可证伪**:<具体核验路径> +``` + +## 与相邻 Skill 的分工 + +``` +epistemic-integrity ←→ 外部世界 (outward) +logical-reasoning ←→ 自身论证 (inward) +problem-analysis ←→ 问题本身 (upfront) +``` + +一个回答可以逻辑自洽但事实有误(GR-010 通过,GR-011 失败),反之亦然。两者正交。 + +## 加载方式 + +所有含事实性断言的回答默认加载。纯主观偏好、纯机械执行时跳过。 diff --git a/skills-engineering/docs/ios-engineer.md b/skills-engineering/docs/ios-engineer.md new file mode 100644 index 0000000..073fa15 --- /dev/null +++ b/skills-engineering/docs/ios-engineer.md @@ -0,0 +1,78 @@ +# ios-engineer 使用文档 + +## 概述 + +`ios-engineer` 是 skills-engineering 的主技能,覆盖 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务中的架构、并发、网络、UI、性能、测试、审查、迁移和发布风险控制。 + +## 核心能力 + +### 1. 认知对手模式 +当涉及技术决策、架构取舍、根因归因、审查最终判断或用户强烈确信时,自动启动认知校准流程(Step 0–6),优先接近真实而非维持对话和谐。 + +### 2. 智能任务分流 +基于 18 条 ROUTE 规则和 7 条 SYM 症状映射,精确路由到最相关的 2–4 份 reference 文件,控制上下文规模。例如: +- Crash / 崩溃 → ROUTE-001(根因排障) +- 架构设计 / 模块拆分 → ROUTE-002(架构设计) +- 网络问题 → ROUTE-008(网络模式) +- 代码审查 → ROUTE-011(审查清单) + +### 3. 四段式输出 +所有回答遵循:根因 → 为什么 → 修法 → 验证 + +### 4. 版本前提声明 +涉及并发、SwiftUI 行为、可用性 API 时,自动输出显式版本前提(从工程读取或显式假设)。 + +### 5. 残留风险声明 +任何改动必须声明:已覆盖 / 未覆盖 / 残留风险。 + +## 加载方式 + +Skill 文件结构: +- `SKILL.md` — 技能主入口 +- `AGENT-BRIEF.md` — Agent 快速决策参考 +- `references/` — 28 份按主题拆分的规则细则 + +Agent 自动加载流程: +1. 读 `AGENT-BRIEF.md` 判断是否命中 +2. 命中后读 `SKILL.md` 全文 +3. 按 ROUTE 表加载相关 reference 文件 + +## 常见场景 + +### 崩溃排障 +用户描述:线上用户遇到崩溃 +→ Agent 加载:root_cause_enforcement.md + swift_concurrency.md(如涉及并发) + +### 架构设计 +用户描述:想重构首页,把 MVC 改成 MVVM +→ Agent 加载:architecture_and_network.md + migration_strategy.md + +### 代码审查 +用户描述:帮我看下这个 PR +→ Agent 加载:review_checklists.md + anti_patterns.md + ios_conventions.md + +### 性能优化 +用户描述:列表滚动卡顿 +→ Agent 加载:performance_optimization.md + observability_logging.md + +## 迁移与同步 + +```bash +# 同步 ios-engineer 到各 Agent 目录 +./scripts/sync-skills.sh + +# 同步 Agent preamble(包括 ios-engineer 加载指令) +./scripts/sync-agent-preamble.sh +``` + +同步目标:`~/.codex/skills/ios-engineer`、`~/.claude/skills/ios-engineer`、`~/.cursor/skills/ios-engineer`、`~/.gemini/skills/ios-engineer`。 + +## 演进治理 + +规则变更通过受控演进流程: +1. 创建 proposal +2. 运行校验 +3. 记录验证与审批 +4. 执行晋升 + +详见 [README.md](../README.md) 的「演进工作流」章节。 diff --git a/skills-engineering/docs/logical-reasoning.md b/skills-engineering/docs/logical-reasoning.md new file mode 100644 index 0000000..74a6e50 --- /dev/null +++ b/skills-engineering/docs/logical-reasoning.md @@ -0,0 +1,47 @@ +# logical-reasoning 使用文档 + +## 概述 + +`logical-reasoning` 是全局论证纪律 skill,约束 AI **自身回答**的论证质量。确保每条回复的逻辑链可追溯、层级分明、因果克制。 + +## 核心规则 + +GR-010 定义了以下要求: + +### 可追溯逻辑链 +每条推理必须有迹可循,从结论回溯到前提。 + +### 四层区分 +- **事实**:可核验的客观信息 +- **推断**:基于事实推导的判断(标明不确定性) +- **建议**:主观推荐(不是必然结论) +- **推测**:低证据强度的猜想(须显式标注) + +### 禁止行为 +- 无依据的因果跳跃 +- 循环论证 +- 同一回复内自相矛盾 +- 用流畅措辞伪装确定性 + +## 逻辑链块 + +高风险判断(技术决策、架构取舍、根因归因等)输出时,必须包含独立「逻辑链」块: + +``` +## 逻辑链 +- **事实/证据**:<可核验依据> +- **推断**:<基于事实的推导> +- **结论强度**:<强/中/弱> +- **可证伪/缺口**:<什么情况下结论会被推翻> +``` + +## 与认知对手模式的分工 + +| 角色 | 目标 | 负责 | +|------|------|------| +| 认知对手模式 | 挑战**用户**结论的逻辑 | ios-engineer | +| GR-010(本 skill) | 约束**AI**自身的论证 | logical-reasoning | + +## 加载方式 + +所有含判断成分的任务默认加载。纯机械执行时跳过。 diff --git a/skills-engineering/docs/problem-analysis.md b/skills-engineering/docs/problem-analysis.md new file mode 100644 index 0000000..b255c95 --- /dev/null +++ b/skills-engineering/docs/problem-analysis.md @@ -0,0 +1,39 @@ +# problem-analysis 使用文档 + +## 概述 + +`problem-analysis` 是问题前置分析 skill,确保 AI 在**回答之前**充分理解问题、检验逻辑、拆解真实需求。它是所有含判断任务的第一道门控。 + +## 核心规则 + +| 规则 ID | 规则名称 | 说明 | +|---------|---------|------| +| PA-001 | 逻辑检验 | 审查问题是否含逻辑错误、矛盾前提、循环假设或虚假二分 | +| PA-002 | 第一性原理 | 从底层需求拆解——实际要解决什么?当前路径是否最优? | +| PA-003 | 理解门控 | PA-001 + PA-002 完成前不开始正式回复 | + +## 问题分析块 + +发现问题偏差或更优路径时,输出独立的「问题分析」块: + +``` +## 问题分析 +- **逻辑问题**:<指出的逻辑错误> +- **真实需求**:<通过第一性原理分析出的底层需求> +- **更优路径**:<建议的替代方案> +``` + +问题清晰时静默通过,不输出额外内容。 + +## 与其他 Skill 的分工 + +| Skill | 动作时机 | 关注点 | +|-------|---------|--------| +| problem-analysis | 回答**之前** | 问题的合理性 | +| logical-reasoning | 回答**之中** | 自身论证质量 | +| epistemic-integrity | 回答**之后** | 结论的外部验证 | +| engineering-discipline | 全流程 | 输出结构纪律 | + +## 加载方式 + +收到任何技术问题、方案讨论、实现请求、架构取舍时默认加载。纯机械执行时跳过。 diff --git a/skills-engineering/engineering-discipline/AGENT-BRIEF.md b/skills-engineering/engineering-discipline/AGENT-BRIEF.md new file mode 100644 index 0000000..e50711e --- /dev/null +++ b/skills-engineering/engineering-discipline/AGENT-BRIEF.md @@ -0,0 +1,25 @@ +# engineering-discipline Agent 调用指南 + +## 一句话描述 + +全局工程纪律——安全合规防御、前置确认、单根因、四段式、最小修复、预算拦截、防 Diff 噪声、残留风险声明(GR-001…008)。适用所有工程任务,不限平台。 + +## 何时调用 + +**默认加载**:所有工程类任务(含排障、设计、实现、审查)。 + +## 关键行为 + +1. **[GR-001]** 绝不读取/打印/提交敏感机密;高风险 shell 命令前安全自检。 +2. **[GR-002]** 描述不清时先输出独立"前置确认"块。 +3. **[GR-003]** 锁定 1 个最高概率根因,最多 1 个备选。 +4. **[GR-004]** 按"根因 → 为什么 → 修法 → 验证"四段式输出。 +5. **[GR-005]** 先给最小可验证修复。 +6. **[GR-006]** 连续失败 3 次或 turn 数超 15 次时主动中断确认。 +7. **[GR-007]** 不格式化代码(除非明确要求);自动修复限于 Staged 变更。 +8. **[GR-008]** 任何改动声明"已覆盖/未覆盖/残留风险"。 + +## 不调用的情况 + +- 纯闲聊 +- 无任何改动或判断成分的机械执行 diff --git a/skills-engineering/engineering-discipline/OUT-OF-SCOPE.md b/skills-engineering/engineering-discipline/OUT-OF-SCOPE.md new file mode 100644 index 0000000..ef6afc0 --- /dev/null +++ b/skills-engineering/engineering-discipline/OUT-OF-SCOPE.md @@ -0,0 +1,17 @@ +# engineering-discipline 范围外 + +本 skill 提供**全局工程纪律**,是对所有工程任务的**通用约束层**。它不负责特定平台/框架的具体问题。 + +## 不处理的内容 + +- **平台特定技术问题**:iOS、Android、Web 等具体平台的实现细节由对应平台 skill 负责。本 skill 只约束工程回答的结构和纪律,不替代领域知识。 +- **纯创造性/非工程任务**:撰写文学内容、艺术创作、纯翻译等非代码任务不在范围内。但如果这些任务涉及技术工程(如生成前端代码),本 skill 的纪律仍然适用。 +- **战略/商业决策**:产品路线图、商业策略、营销等非工程决策不在范围内。 + +## 边界说明 + +本 skill 的 GR-001 到 GR-008 规则是**正交层**——它们定义的是"如何输出",而非"输出什么": +- `ios-engineer` 定义 iOS 工程领域知识 +- `engineering-discipline` 定义工程输出必须遵循的结构纪律 + +两者同时命中时并行执行,互不冲突。 diff --git a/skills-engineering/epistemic-integrity/AGENT-BRIEF.md b/skills-engineering/epistemic-integrity/AGENT-BRIEF.md new file mode 100644 index 0000000..36400e8 --- /dev/null +++ b/skills-engineering/epistemic-integrity/AGENT-BRIEF.md @@ -0,0 +1,24 @@ +# epistemic-integrity Agent 调用指南 + +## 一句话描述 + +全局真值接地纪律——不把未验证内容说成已知、自信≠正确、逼出可验证物、验证方法论与求真方法边界(GR-011/012/013)。 + +## 何时调用 + +- **默认**:任何含事实性断言的回答、解惑型问题、「X 是什么/怎么做/对不对」、方案中作为依据的事实前提。 +- **必须输出验证锚点块**:用户据此决策且错误代价高;用户问「怎么验证/可信吗」;训练截止之后或长尾领域的事实判断。 +- **跳过**:纯主观偏好/创作、纯机械执行、用户明确「只要快速估计、不必核」。 + +## 关键行为 + +1. **[GR-011] 反幻觉接地**:高危带默认降置信,能用工具就不靠记忆。关键事实必须给来源或「怎么核」的把手。 +2. **[GR-012] 验证方法论**:现实当裁判(可跑就跑、可查一手文档就查)>有问责一手源>独立交叉。优先证伪而非穷尽确认。 +3. **[GR-013] 求真方法边界**:事实类查证(不推导),推理类允许第一性原理。校准把握度而非消除语气。 +4. 高风险事实结论输出独立「验证锚点」块(结论/依据来源/置信度/怎么核·可证伪)。 + +## 不调用的情况 + +- 纯主观偏好/创作 +- 纯机械执行 +- 用户显式跳过验证 diff --git a/skills-engineering/epistemic-integrity/OUT-OF-SCOPE.md b/skills-engineering/epistemic-integrity/OUT-OF-SCOPE.md new file mode 100644 index 0000000..253b5c6 --- /dev/null +++ b/skills-engineering/epistemic-integrity/OUT-OF-SCOPE.md @@ -0,0 +1,17 @@ +# epistemic-integrity 范围外 + +本 skill 负责**真值接地**——确保结论可被外部验证、自信与正确性匹配。不负责回答的技术正确性本身。 + +## 不处理的内容 + +- **技术内容的正确性**:本 skill 定义验证方法论,但不替代具体领域知识。iOS 的具体技术正确性由 `ios-engineer` 负责。 +- **论证的内部自洽性**:GR-010(逻辑链内部自洽)由 `logical-reasoning` skill 负责,本 skill 关注的是结论与**外部真实世界**的接地。 +- **问题的前置分析**:问题的逻辑有效性检验由 `problem-analysis` skill 负责。 + +## 分工边界 + +| Skill | 方向 | 职责 | +|-------|------|------| +| `epistemic-integrity`(本 skill) | outward | 结论与世界是否相符、怎么去核 | +| `logical-reasoning`(GR-010) | inward | 回复自身是否自洽、分层、不确定标清 | +| `problem-analysis`(PA-001/002) | upfront | 问题本身合理性、第一性原理拆解 | diff --git a/skills-engineering/ios-engineer/AGENT-BRIEF.md b/skills-engineering/ios-engineer/AGENT-BRIEF.md new file mode 100644 index 0000000..040ac2e --- /dev/null +++ b/skills-engineering/ios-engineer/AGENT-BRIEF.md @@ -0,0 +1,35 @@ +# ios-engineer Agent 调用指南 + +## 一句话描述 + +iOS / Swift / SwiftUI / UIKit / Xcode 工程全生命周期——架构、并发、网络、性能、崩溃调试、代码审查、重构、迁移与测试。 + +## 何时调用 + +代理应在以下任一场景自动加载本 skill: + +| 触发信号 | 示例 | +|---------|------| +| 平台关键词 | iOS、iPhone、iPad、macOS (Catalyst)、Apple Watch、Apple TV、Xcode | +| 语言/框架关键词 | Swift、SwiftUI、UIKit、Objective-C、Combine、async/await | +| 问题类型 | 崩溃、Crash、内存泄漏、卡顿、布局错位、约束冲突 | +| 工程关键词 | CocoaPods、SPM、Carthage、Xcode build、TestFlight、App Store | +| 审查/迁移 | 代码审查(PR Review)、重构、迁移、架构升级 | + +## 关键行为 + +1. **认知对手模式**:技术决策/架构取舍/根因归因时,严格按 `cognitive_adversary_mode.md` 执行 Step 0–6。 +2. **版本前提**:涉及并发/可用性 API/SwiftUI 行为时,必须输出显式版本前提声明。 +3. **任务分流**:按 ROUTE 表精确路由,默认只加载 2–4 份 reference,控制上下文规模。 +4. **四段式输出**:根因 → 为什么 → 修法 → 验证。 +5. **残留风险声明**:任何改动必须声明已覆盖/未覆盖/残留风险。 + +## 不调用的情况 + +- 非 Apple 平台开发 +- 后端服务/API 实现 +- Web 前端开发 +- 纯设计/UX 讨论 +- 非技术内容 + +详见 `OUT-OF-SCOPE.md`。 diff --git a/skills-engineering/ios-engineer/OUT-OF-SCOPE.md b/skills-engineering/ios-engineer/OUT-OF-SCOPE.md new file mode 100644 index 0000000..739533b --- /dev/null +++ b/skills-engineering/ios-engineer/OUT-OF-SCOPE.md @@ -0,0 +1,27 @@ +# ios-engineer 范围外 + +本 skill 专注于 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务。 + +## 不处理的内容 + +- **非 Apple 平台开发**:Android、Flutter、React Native、Kotlin Multiplatform 等跨平台框架的问题不在本 skill 范围内。建议使用对应平台的专用 skill。 +- **后端服务开发**:API 设计、数据库 schema、服务端部署等问题不在范围内(可通过 MCP apifox 工具配合网络协议调用来确认接口契约,但不负责后端实现)。 +- **Web 前端开发**:HTML / CSS / JavaScript / React / Vue 等 Web 技术栈问题不在范围内。 +- **通用 DevOps**:非 iOS 相关的 CI/CD 管道、Docker、Kubernetes 配置不在范围内。 +- **纯设计/UX 讨论**:不含代码实现的 Figma、Sketch 设计评审或纯 UI/UX 方法论讨论不在范围内。 +- **非技术内容**:项目管理、团队组织、职业发展等非技术话题不在范围内。 + +## 边界判据 + +| 问题类型 | 是否处理 | 替代方案 | +|---------|---------|---------| +| iOS 应用崩溃排查 | 是 | — | +| Swift 并发问题 | 是 | — | +| Android 崩溃排查 | 否 | 使用对应平台 skill | +| REST API 服务端实现 | 否 | 使用后端 skill | +| React 组件实现 | 否 | 使用前端 skill | +| Figma 设计评审 | 否 | 使用设计 skill | + +## 澄清策略 + +遇到范围不明确的问题时,先确认是否为 iOS 平台相关。如判定超出范围,明确告知用户并建议合适的替代方案。 diff --git a/skills-engineering/logical-reasoning/AGENT-BRIEF.md b/skills-engineering/logical-reasoning/AGENT-BRIEF.md new file mode 100644 index 0000000..8544708 --- /dev/null +++ b/skills-engineering/logical-reasoning/AGENT-BRIEF.md @@ -0,0 +1,26 @@ +# logical-reasoning Agent 调用指南 + +## 一句话描述 + +全局论证纪律——可追溯逻辑链、层级分明、因果克制、逻辑链输出块(GR-010)。适用所有工程任务,不限平台。 + +## 何时调用 + +- **默认**:所有含判断成分的任务。 +- **必须输出逻辑链块**:技术决策、架构取舍、根因归因、性能归因、审查最终判断、用户强烈确信或显式要求挑战观点。 +- **跳过**:纯机械执行、无任何判断成分的任务。 + +## 关键行为 + +1. **[GR-010]** 回复必须具备可追溯的逻辑链。 +2. 区分「事实 / 推断 / 建议 / 推测」,不得把未验证推断写成定论。 +3. 禁止无依据的因果跳跃、循环论证、同一回复内自相矛盾。 +4. 非显然判断至少标出一步「因为…所以…」。 +5. 证据不足时标明不确定,不得用流畅措辞伪装确定性。 +6. 高风险判断输出独立「逻辑链」块(事实/证据、推断、结论强度、可证伪/缺口)。 + +## 不调用的情况 + +- 纯机械执行 +- 无任何判断成分(纯信息复述) +- 纯主观偏好/创作 diff --git a/skills-engineering/logical-reasoning/OUT-OF-SCOPE.md b/skills-engineering/logical-reasoning/OUT-OF-SCOPE.md new file mode 100644 index 0000000..d960a1f --- /dev/null +++ b/skills-engineering/logical-reasoning/OUT-OF-SCOPE.md @@ -0,0 +1,20 @@ +# logical-reasoning 范围外 + +本 skill 约束 AI **自身回复**的论证质量(inward),不负责检验用户提问逻辑或结论真实性。 + +## 不处理的内容 + +- **用户提问的逻辑检验**:由 `problem-analysis`(PA-001)负责。 +- **结论与外部世界的接地**:由 `epistemic-integrity`(GR-011/012)负责。 +- **认知对手模式**:由 `ios-engineer/references/cognitive_adversary_mode.md` 负责(挑战用户结论)。 +- **工程输出结构**:由 `engineering-discipline`(GR-004 四段式)负责。 + +## 边界说明 + +GR-010 是 inward 约束: +- **逻辑链可追溯**:每步推理都能回到上游前提 +- **四层区分**:事实 / 推断 / 建议 / 推测 +- **强度匹配**:结论强度不超出证据强度 +- **不矛盾**:同一回复内部不自相冲突 + +与 GR-011/012 正交——一条回复可以内部逻辑自洽但与外部世界不符,也可以方向正确但论证结构混乱。两者同时命中时并行执行。 diff --git a/skills-engineering/problem-analysis/AGENT-BRIEF.md b/skills-engineering/problem-analysis/AGENT-BRIEF.md new file mode 100644 index 0000000..42b5ff6 --- /dev/null +++ b/skills-engineering/problem-analysis/AGENT-BRIEF.md @@ -0,0 +1,22 @@ +# problem-analysis Agent 调用指南 + +## 一句话描述 + +问题前置分析——逻辑检验、第一性原理拆解、充分理解后再回复(PA-001/002/003)。适用所有含判断或方案讨论的任务。 + +## 何时调用 + +- **默认**:收到任何技术问题、方案讨论、实现请求、架构取舍时。 +- **跳过**:纯机械执行(格式化代码、直接翻译)、无判断成分的信息复述。 + +## 关键行为 + +1. **[PA-001] 逻辑检验**:收到问题后先审查是否含逻辑错误、矛盾前提、循环假设或虚假二分。若发现须先揭示,不得在错误前提上直接作答。 +2. **[PA-002] 第一性原理**:从底层需求拆解——实际要解决的是什么?当前路径是否最优?若存在更优解或更深层需求,必须在正式回复前点明。 +3. **[PA-003] 理解门控**:PA-001 + PA-002 完成前不开始正式回复。问题清晰时内部完成即可;发现偏差时输出「问题分析」块。 + +## 不调用的情况 + +- 纯机械执行 +- 无判断成分的信息复述 +- 纯翻译/格式化任务 diff --git a/skills-engineering/problem-analysis/OUT-OF-SCOPE.md b/skills-engineering/problem-analysis/OUT-OF-SCOPE.md new file mode 100644 index 0000000..78a41cf --- /dev/null +++ b/skills-engineering/problem-analysis/OUT-OF-SCOPE.md @@ -0,0 +1,22 @@ +# problem-analysis 范围外 + +本 skill 负责**问题前置分析**——在回答前检验问题逻辑、拆解真实需求。不负责回答内容本身或结论验证。 + +## 不处理的内容 + +- **回答内容的正确性**:由对应领域 skill 负责。 +- **结论的外部验证**:由 `epistemic-integrity`(GR-011/012)负责。 +- **回答的论证结构**:由 `logical-reasoning`(GR-010)负责。 +- **工程输出结构**:由 `engineering-discipline`(GR-002/004)负责。 +- **纯机械执行**:格式化代码、直接翻译等无判断成分的任务不需要前置分析。 + +## 边界说明 + +PA-001/002/003 是 upfront 门控: +- 问题逻辑检验在回答**之前**完成 +- 发现偏差时输出独立「问题分析」块 +- 问题清晰时静默通过 + +与 GR-010 的分工: +- GR-010 约束 AI **自身回复**的论证质量 +- PA-001 检验**用户问题**的逻辑有效性 diff --git a/skills-engineering/scripts/list-skills.sh b/skills-engineering/scripts/list-skills.sh new file mode 100755 index 0000000..3f90273 --- /dev/null +++ b/skills-engineering/scripts/list-skills.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# List all registered skills in the skills-engineering repository. +# Outputs skill name, path, and description (from frontmatter). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +echo "=== skills-engineering — 已注册技能 ===" +echo "" + +format=" %-30s %-60s\n" +printf "$format" "SKILL" "DESCRIPTION" +printf "$format" "-----" "-----------" + +for d in "${SE_DIR}"/*/; do + skill_file="${d}SKILL.md" + if [[ ! -f "$skill_file" ]]; then + continue + fi + skill_name="$(basename "${d}")" + + # Extract description from YAML frontmatter + description="" + in_frontmatter=false + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if $in_frontmatter; then + break + else + in_frontmatter=true + continue + fi + fi + if $in_frontmatter; then + if [[ "$line" =~ ^description: ]]; then + # Handle multi-line descriptions (with >- or >) + if [[ "$line" =~ \>[-]?$ ]]; then + # Multi-line: read the next line for content + if IFS= read -r next_line; then + description="${next_line#"${next_line%%[![:space:]]*}"}" + fi + else + description="$(echo "$line" | sed 's/^description: *//')" + fi + break + fi + fi + done < "$skill_file" + + # Truncate to 55 chars for display + if [[ ${#description} -gt 55 ]]; then + description="${description:0:52}..." + fi + + printf "$format" "$skill_name" "$description" +done + +echo "" +echo "Total: $(find "${SE_DIR}" -maxdepth 2 -name SKILL.md | wc -l | tr -d ' ') skills" +echo "" +echo "运行 'cat /AGENT-BRIEF.md' 查看快速决策参考。" +echo "运行 'cat /SKILL.md' 查看完整规则。" diff --git a/skills-engineering/scripts/sync-skills.sh b/skills-engineering/scripts/sync-skills.sh index 9831533..b7cb472 100755 --- a/skills-engineering/scripts/sync-skills.sh +++ b/skills-engineering/scripts/sync-skills.sh @@ -174,6 +174,8 @@ sync_one_skill_to_target() { local rsync_flags=(-a --delete --delete-excluded \ --include "/SKILL.md" \ + --include "/AGENT-BRIEF.md" \ + --include "/OUT-OF-SCOPE.md" \ --include "/references/" --include "/references/**" \ --exclude "*") if [[ "${DRY_RUN}" == "true" ]]; then diff --git a/skills-engineering/scripts/templates/epistemic-integrity.mdc.tmpl b/skills-engineering/scripts/templates/epistemic-integrity.mdc.tmpl new file mode 100644 index 0000000..2faca8f --- /dev/null +++ b/skills-engineering/scripts/templates/epistemic-integrity.mdc.tmpl @@ -0,0 +1,6 @@ +--- +description: 全局真值接地纪律:反幻觉接地、验证方法论、求真方法边界(GR-011/012/013) +alwaysApply: true +--- + + diff --git a/skills-engineering/scripts/verify-sync.sh b/skills-engineering/scripts/verify-sync.sh index f7d4c7a..41d72b4 100755 --- a/skills-engineering/scripts/verify-sync.sh +++ b/skills-engineering/scripts/verify-sync.sh @@ -51,6 +51,8 @@ check_skill_dir() { return fi [[ -f "$dir/SKILL.md" ]] || note_fail "$dir/SKILL.md missing" + [[ -f "$dir/AGENT-BRIEF.md" ]] || note_fail "$dir/AGENT-BRIEF.md missing" + [[ -f "$dir/OUT-OF-SCOPE.md" ]] || note_fail "$dir/OUT-OF-SCOPE.md missing" [[ -d "$dir/references" ]] || note_fail "$dir/references/ missing" for stale in evolution proposals history scripts agents validations scenarios approvals usage; do if [[ -d "$dir/$stale" ]]; then @@ -86,6 +88,12 @@ check_preamble_tilde() { if ! grep -q 'engineering-discipline/references/engineering_discipline.md' "$file"; then note_fail "$file missing engineering-discipline full-text load instruction" fi + if ! grep -q 'problem-analysis/references/problem_analysis.md' "$file"; then + note_fail "$file missing problem-analysis full-text load instruction" + fi + if ! grep -q 'epistemic-integrity/references/epistemic_integrity.md' "$file"; then + note_fail "$file missing epistemic-integrity full-text load instruction" + fi } CHECKED=0 @@ -148,7 +156,7 @@ if [[ $FAIL -eq 0 ]]; then if [[ $CHECKED -eq 0 ]]; then echo "OK: no sync targets enabled; nothing to verify." else - echo "OK: ${CHECKED} target(s) clean (all skills: SKILL.md + references/ only); preambles tilde-ified" + echo "OK: ${CHECKED} target(s) clean (all skills: SKILL.md + AGENT-BRIEF.md + OUT-OF-SCOPE.md + references/); preambles tilde-ified" fi fi exit $FAIL From 160ac75bb553cddeaa38d5fc3250378483dd4618 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 16:23:31 +0800 Subject: [PATCH 15/30] chore(skills-engineering): remove obsolete CHANGELOG, CONTEXT, and COMPARISON-REPORT files - Deleted CHANGELOG.md, CONTEXT.md, and COMPARISON-REPORT.md as part of repository cleanup. - These files were previously used for documentation and comparison but are no longer needed. --- skills-engineering/CHANGELOG.md | 19 ---- skills-engineering/COMPARISON-REPORT.md | 143 ------------------------ skills-engineering/CONTEXT.md | 59 ---------- 3 files changed, 221 deletions(-) delete mode 100644 skills-engineering/CHANGELOG.md delete mode 100644 skills-engineering/COMPARISON-REPORT.md delete mode 100644 skills-engineering/CONTEXT.md diff --git a/skills-engineering/CHANGELOG.md b/skills-engineering/CHANGELOG.md deleted file mode 100644 index 6045c03..0000000 --- a/skills-engineering/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -# skills-engineering Changelog - -## [Unreleased] - -### Added (2026-07-05) -- **AGENT-BRIEF.md**: 为 cognitive-expansion、engineering-discipline、epistemic-integrity、ios-engineer、logical-reasoning、problem-analysis 六个 skill 添加 Agent 快速决策参考 -- **OUT-OF-SCOPE.md**: 为所有六个 skill 添加职责边界声明,明确不处理的内容 -- **.claude-plugin/plugin.json**: Claude Code 插件清单,支持一键安装 -- **.agents/**: Agent 调用规范与文档写作规范 -- **.out-of-scope/**: 仓库级范围外声明 -- **docs/**: 每个 skill 的独立使用文档 -- **CONTEXT.md**: 仓库用途与快速上手指南 -- **CHANGELOG.md**: 仓库变更日志(本文件) -- **COMPARISON-REPORT.md**: 与 mattpocock/skills 开源库的深度对比分析报告 -- **list-skills.sh**: 列出所有已注册 skill 及描述 - -### Fixed -- `verify-sync.sh`: 补全 epistemic-integrity 和 problem-analysis 的 preamble 检查 -- 创建缺失的 `epistemic-integrity.mdc.tmpl` 模板,补齐 Cursor 生成链路 diff --git a/skills-engineering/COMPARISON-REPORT.md b/skills-engineering/COMPARISON-REPORT.md deleted file mode 100644 index d5f73fe..0000000 --- a/skills-engineering/COMPARISON-REPORT.md +++ /dev/null @@ -1,143 +0,0 @@ -# skills-engineering vs mattpocock/skills 深度对比分析报告 - -> 分析日期:2026-07-05 - ---- - -## 一、mattpocock/skills 开源库功能分析 - -该仓库由 TypeScript 专家 Matt Pocock 维护,是一个面向 AI Agent(尤其是 Claude Code)的 **Skill 集合和发布工具链**。核心特点: - -| 维度 | 描述 | -|------|------| -| **Skill 结构** | `SKILL.md`(主文件)+ `AGENT-BRIEF.md`(Agent 速览)+ `OUT-OF-SCOPE.md`(范围外) | -| **分类体系** | `engineering/`、`productivity/`、`misc/`、`personal/`、`in-progress/`、`deprecated/` | -| **插件发布** | `.claude-plugin/plugin.json` — 可作为 Claude Code 插件一键安装 | -| **文档深度** | 每个 skill 有独立的 `docs/.md` | -| **Agent 治理** | `.agents/` 目录包含调用规范和文档写作规范 | -| **仓库范围** | `.out-of-scope/` 声明跨 skill 通用约束 | -| **工程化** | `package.json` + npm 发布 + changeset 版本管理 | -| **工具脚本** | `list-skills.sh`、`link-skills.sh` | - -### Skill 清单 - -#### engineering/ (工程类) -- `triage` — 快速分诊 bug 报告,不做深入修复 -- `code-review` — 代码审查,严格检查清单 -- `implement` — 从 spec → implementation 全程 -- `tdd` — 测试驱动开发 -- `diagnosing-bugs` — 系统化 bug 定位 -- `research` — 技术调研与方案对比 -- `domain-modeling` — 领域建模 -- `improve-codebase-architecture` — 架构改进 -- `resolving-merge-conflicts` — 合并冲突解决 -- `to-prd` — 需求转 PRD - -#### productivity/ (生产力类) -- `handoff` — 工作交接记录,确保上下文不丢失 -- `grill-me` / `grilling` — 追问-反驳式审查 -- `teach` — 以教代学的解释模式 -- `writing-great-skills` — Skill 写作方法论(元 Skill) - -#### misc/ (杂项) -- `git-guardrails-claude-code` — Git 安全护栏 - ---- - -## 二、skills-engineering 的独特优势(mattpocock 没有的) - -skills-engineering 在以下方面**远超** mattpocock/skills: - -1. **受控演进流水线**(mattpocock 只用 changeset,无 governance) -2. **规则 ID 体系**(IR/SYM/ROUTE/OUT/GR/PA)及 `rule_index.md` 索引 -3. **多端自动同步**(Codex/Claude/Cursor/Gemini/Xcode 一键同步) -4. **Pre-commit/Pre-push 守卫**(规则变更必须绑定治理记录) -5. **Usage Ledger**(使用观测与效果评估) -6. **认知对手模式**(Step 0–6 全链条反迎合机制) -7. **ROUTE 精确定向**(症状 → 路由 → 仅加载 2–4 份 reference) -8. **12 类校验**(`validate_skill_evolution.sh` 伞形入口) - ---- - -## 三、发现的问题与修复 - -### 🐛 问题 1:`verify-sync.sh` preamble 检查不完整 - -`check_preamble_tilde()` 函数只检查了 cognitive-expansion、logical-reasoning、engineering-discipline 三个 skill 的 preamble 引用,漏掉了 epistemic-integrity 和 problem-analysis。 - -**修复**:已在 `verify-sync.sh` 第 89–94 行补全。 - -### 🐛 问题 2:`epistemic-integrity.mdc.tmpl` 模板缺失 - -sync-manifest 中注册了 `skill:epistemic-integrity`,但 `scripts/templates/` 下没有对应的 `.mdc.tmpl`。 - -**修复**:已创建该模板,补齐了 Cursor `.mdc` 生成链路。 - ---- - -## 四、从 mattpocock/skills 补全的新增功能 - -| 新增内容 | 说明 | -|---------|------| -| **各 skill 的 `AGENT-BRIEF.md`**(6 个) | Agent 快速决策参考:触发条件、关键行为、不调用情况 | -| **各 skill 的 `OUT-OF-SCOPE.md`**(6 个) | 明确声明 skill 不处理的内容,防止误触发 | -| **`.claude-plugin/plugin.json`** | Claude Code 插件清单,支持一键安装为 Claude 插件 | -| **`.agents/invocation.md`** | Agent 调用规范与多 skill 并行加载流程 | -| **`.agents/writing-docs.md`** | 文档写作规范(命名、ID 格式、结构约定) | -| **`.out-of-scope/repository-scope.md`** | 仓库级范围外声明(安全合规、问题数量限制等) | -| **`docs/*.md`**(6 个) | 每个 skill 的独立使用文档 | -| **`CONTEXT.md`** | 仓库用途与快速上手指南(给人类读) | -| **`CHANGELOG.md`** | 仓库变更日志(与 skill 内部的 evolution 历史互补) | -| **`list-skills.sh`** | 列出所有已注册 skill 及描述 | - ---- - -## 五、功能对比总览 - -| 功能 | mattpocock/skills | skills-engineering(修复前) | skills-engineering(修复后) | -|------|:---:|:---:|:---:| -| SKILL.md 主入口 | ✅ | ✅ | ✅ | -| AGENT-BRIEF.md | ✅ | ❌ | ✅ | -| OUT-OF-SCOPE.md | ✅ | ❌ | ✅ | -| .claude-plugin/plugin.json | ✅ | ❌ | ✅ | -| .agents/ 调用指南 | ✅ | ❌(仅有 openai.yaml) | ✅ | -| .out-of-scope/ 仓库约束 | ✅ | ❌ | ✅ | -| docs/ 使用文档 | ✅ | ❌ | ✅ | -| CONTEXT.md | ✅ | ❌ | ✅ | -| CHANGELOG.md | ✅ | ❌ | ✅ | -| list-skills.sh | ✅ | ❌ | ✅ | -| 受控演进 governance | ❌ | ✅ | ✅ | -| 规则 ID 体系 | ❌ | ✅ | ✅ | -| 多端自动同步 | ❌ | ✅ | ✅ | -| 认知对手模式 | ❌ | ✅ | ✅ | -| Usage Ledger | ❌ | ✅ | ✅ | -| Pre-commit/Pre-push 守卫 | ❌ | ✅ | ✅ | - ---- - -## 六、各自适合的使用场景 - -### 选择 mattpocock/skills 的场景 -- 需要轻量级、开箱即用的工程 skill 集合 -- 使用 TypeScript/Node.js 技术栈 -- 偏好 npm 生态和 changeset 版本管理 -- 需要快速集成到 Claude Code 插件体系 - -### 选择 skills-engineering 的场景 -- 需要跨平台 Agent 同步(Claude/Codex/Cursor/Gemini/Xcode) -- 需要可审计的规则变更治理 -- 需要 iOS/Swift 垂直领域的专业指导 -- 需要"认知对手"模式防止 AI 迎合用户 -- 需要精确的路由和按需加载机制(ROUTE 定向) -- 需要规则使用效果的可观测性(Usage Ledger) - ---- - -## 七、总结 - -两个仓库在设计哲学上有本质区别: - -- **mattpocock/skills** 是"轻量级技能市场"——注重可发现性、可安装性、社区贡献友好 -- **skills-engineering** 是"受控工程规则平台"——注重正确性保证、变更治理、跨平台一致性 - -修复后,skills-engineering 在 **工程规范性** 上已经基本对齐 mattpocock/skills 的仓库结构(补齐了 AGENT-BRIEF、OUT-OF-SCOPE、插件清单、文档等),同时保留了自身在 **治理、同步、路由、反迎合** 方面的核心差异化优势。 diff --git a/skills-engineering/CONTEXT.md b/skills-engineering/CONTEXT.md deleted file mode 100644 index e779b5b..0000000 --- a/skills-engineering/CONTEXT.md +++ /dev/null @@ -1,59 +0,0 @@ -# skills-engineering — 给人类的上下文 - -## 这是什么? - -`skills-engineering` 是一个**受控工程规则平台**,通过 Skill(技能)形式向 AI Agent(Claude Code、Codex、Cursor、Gemini、Xcode)提供领域工程指导和纪律约束。 - -它与 mattpocock/skills 不同——这里是"受控治理",不是"技能市场"。 - -## 快速开始 - -### 1. 查看所有 Skill - -```bash -bash scripts/list-skills.sh -``` - -### 2. 同步到本地 Agent - -```bash -bash scripts/sync-skills.sh -``` - -### 3. 验证同步完整性 - -```bash -bash scripts/verify-sync.sh -``` - -## Skill 分类 - -| Skill | 类型 | 说明 | -|-------|------|------| -| `ios-engineer` | 领域 | iOS/Swift/SwiftUI 工程全流程 | -| `engineering-discipline` | 纪律 | 安全合规、最小修复、防 Diff 噪声 | -| `epistemic-integrity` | 纪律 | 真值接地——不编造、不伪装确定 | -| `logical-reasoning` | 纪律 | 论证链可追溯、因果克制 | -| `problem-analysis` | 前置 | 问题分析——充分理解后再行动 | -| `cognitive-expansion` | 认知 | 打破知识茧房、邻域启发 | - -## 治理模型 - -- **规则 ID 体系**:IR/SYM/ROUTE/OUT/GR/PA 分类标识 -- **受控演进**:规则变更需绑定治理记录 -- **Usage Ledger**:使用观测与效果评估 -- **ROUTE 定向**:症状 → 路由 → 按需加载 - -## 关键文件 - -| 文件 | 用途 | -|------|------| -| `*/SKILL.md` | Skill 主入口 | -| `*/AGENT-BRIEF.md` | Agent 快速决策参考 | -| `*/OUT-OF-SCOPE.md` | 职责范围外声明 | -| `*/references/` | 详细规则文档 | -| `scripts/` | 同步/校验/工具脚本 | -| `docs/` | 使用文档 | -| `.claude-plugin/` | Claude Code 插件配置 | -| `.agents/` | Agent 调用规范 | -| `.out-of-scope/` | 仓库级约束 | From abcbcdd11d9095c149b91f9ad960293a14e84fab Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 17:09:01 +0800 Subject: [PATCH 16/30] =?UTF-8?q?feat(ios-engineer):=20=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=89=A9=E5=B1=95=20=E2=80=94=20=E6=96=B0=E5=A2=9E=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E6=A8=A1=E5=9D=97=E3=80=81=E8=BF=9B=E5=8C=96=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E4=B8=8E=E5=8F=82=E8=80=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 5 个场景规范 (extensions, notifications, persistence, privacy, storekit) - 新增 5 个参考文档 (app_extensions, notifications, persistence, privacy_permissions, storekit_iap) - 新增进化提案 20260705-162659-architecture-expansion-v74 及审批记录 - 新增 evolution/hooks 目录及 validate.sh、gc_evolution_history.sh 脚本 - 迁移 hooks (audit-check-on-stop, ledger-sync-on-stop) 从旧位置删除 - 更新 AGENT-BRIEF.md、SKILL.md 及 rule_index 等核心文件 - 更新 self_evolution、usage_ledger、validation_scenarios 引用文档 - 更新 append_usage_entry、promote_skill_evolution、validate 系列脚本 --- .../ios-engineer/AGENT-BRIEF.md | 3 +- skills-engineering/ios-engineer/SKILL.md | 20 ++- ...705-162659-architecture-expansion-v74.json | 78 +++++++++ .../hooks/audit-check-on-stop.py | 0 .../hooks/audit-check-on-stop.sh | 0 .../hooks/ledger-sync-on-stop.sh | 0 ...60705-162659-architecture-expansion-v74.md | 52 ++++++ .../evolution/scenarios/extensions.json | 47 +++++ .../evolution/scenarios/notifications.json | 48 +++++ .../evolution/scenarios/persistence.json | 47 +++++ .../evolution/scenarios/privacy.json | 47 +++++ .../evolution/scenarios/storekit.json | 47 +++++ .../ios-engineer/references/app_extensions.md | 46 +++++ .../ios-engineer/references/notifications.md | 41 +++++ .../ios-engineer/references/persistence.md | 38 ++++ .../references/privacy_permissions.md | 44 +++++ .../ios-engineer/references/rule_index.md | 7 +- .../ios-engineer/references/self_evolution.md | 36 +++- .../ios-engineer/references/storekit_iap.md | 40 +++++ .../ios-engineer/references/usage_ledger.md | 16 +- .../references/validation_scenarios.md | 88 +++++++++- .../scripts/append_usage_entry.sh | 4 +- .../scripts/gc_evolution_history.sh | 123 +++++++++++++ .../scripts/promote_skill_evolution.sh | 12 ++ .../ios-engineer/scripts/validate.sh | 165 ++++++++++++++++++ .../scripts/validate_scenario_specs.sh | 5 + .../scripts/validate_skill_evolution.sh | 98 +++++++++-- .../scripts/validate_usage_ledger.sh | 2 +- 28 files changed, 1120 insertions(+), 34 deletions(-) create mode 100644 skills-engineering/ios-engineer/evolution/approvals/20260705-162659-architecture-expansion-v74.json rename skills-engineering/ios-engineer/{ => evolution}/hooks/audit-check-on-stop.py (100%) rename skills-engineering/ios-engineer/{ => evolution}/hooks/audit-check-on-stop.sh (100%) rename skills-engineering/ios-engineer/{ => evolution}/hooks/ledger-sync-on-stop.sh (100%) create mode 100644 skills-engineering/ios-engineer/evolution/proposals/20260705-162659-architecture-expansion-v74.md create mode 100644 skills-engineering/ios-engineer/evolution/scenarios/extensions.json create mode 100644 skills-engineering/ios-engineer/evolution/scenarios/notifications.json create mode 100644 skills-engineering/ios-engineer/evolution/scenarios/persistence.json create mode 100644 skills-engineering/ios-engineer/evolution/scenarios/privacy.json create mode 100644 skills-engineering/ios-engineer/evolution/scenarios/storekit.json create mode 100644 skills-engineering/ios-engineer/references/app_extensions.md create mode 100644 skills-engineering/ios-engineer/references/notifications.md create mode 100644 skills-engineering/ios-engineer/references/persistence.md create mode 100644 skills-engineering/ios-engineer/references/privacy_permissions.md create mode 100644 skills-engineering/ios-engineer/references/storekit_iap.md create mode 100755 skills-engineering/ios-engineer/scripts/gc_evolution_history.sh create mode 100755 skills-engineering/ios-engineer/scripts/validate.sh diff --git a/skills-engineering/ios-engineer/AGENT-BRIEF.md b/skills-engineering/ios-engineer/AGENT-BRIEF.md index 040ac2e..1be05f0 100644 --- a/skills-engineering/ios-engineer/AGENT-BRIEF.md +++ b/skills-engineering/ios-engineer/AGENT-BRIEF.md @@ -13,7 +13,7 @@ iOS / Swift / SwiftUI / UIKit / Xcode 工程全生命周期——架构、并发 | 平台关键词 | iOS、iPhone、iPad、macOS (Catalyst)、Apple Watch、Apple TV、Xcode | | 语言/框架关键词 | Swift、SwiftUI、UIKit、Objective-C、Combine、async/await | | 问题类型 | 崩溃、Crash、内存泄漏、卡顿、布局错位、约束冲突 | -| 工程关键词 | CocoaPods、SPM、Carthage、Xcode build、TestFlight、App Store | +| 工程关键词 | CocoaPods、SPM、Carthage、Xcode build、TestFlight、App Store、WidgetKit、App Extensions、小组件 | | 审查/迁移 | 代码审查(PR Review)、重构、迁移、架构升级 | ## 关键行为 @@ -29,6 +29,7 @@ iOS / Swift / SwiftUI / UIKit / Xcode 工程全生命周期——架构、并发 - 非 Apple 平台开发 - 后端服务/API 实现 - Web 前端开发 +- 通用 DevOps(Docker / Kubernetes / 非 iOS 相关 CI) - 纯设计/UX 讨论 - 非技术内容 diff --git a/skills-engineering/ios-engineer/SKILL.md b/skills-engineering/ios-engineer/SKILL.md index 5e2512f..d5d27e9 100644 --- a/skills-engineering/ios-engineer/SKILL.md +++ b/skills-engineering/ios-engineer/SKILL.md @@ -14,7 +14,7 @@ description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - **与认知拓展分工**:未命中本模式适用场景时,主答后按 `cognitive-expansion` skill 全文执行;`【深潜】`/`【拓展】` 加深拓展,不与 Step 0–6 重复堆砌。 ## 核心铁律 -- [IR-001] 始终使用简体中文。 +- [IR-001] 始终使用简体中文。例外条款:代码块内的注释、Swift / Objective-C API 名称、编译错误信息字面值、崩溃堆栈、工具命令输出、日志字面值不强制翻译,可保留原文;与用户的对话、方案描述、诊断结论、规则输出、建议说明等自然语言内容仍强制简体中文。 - [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,回答里必须出现一条显式的”版本前提”声明,二选一:要么给出从工程读取的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 真值(如 `iOS 15.0 / Swift 5.9`),要么以”假设 iOS ≥ N / Swift ≥ M,如不符请纠正”形式显式声明假设值。两者缺一或只给其中一项即视为违反本铁律。能读工程时优先读真值;只有在无法读取或成本过高时才允许退到显式假设。本 skill 不预设默认基线。具体落点见 [examples.md](references/examples.md) §1/§2/§4/§5/§6 模板的”版本前提”块与 [review_checklists.md](references/review_checklists.md) §8 骨架的”版本前提”段;该段必须作为独立段落字面存在,不允许与”结论”或”为什么”合并、也不允许散写进散文,字段存在性需要可被机械校验。 - [IR-011] 命中认知对手模式适用场景时,必须输出认知校准结构:复述、最强反驳、隐藏假设、失效条件、可证伪条件、立场翻转、迎合自检、置信度、结论;不得省略最强反驳、立场翻转或迎合自检。完整触发条件、步骤与禁止行为见 [cognitive_adversary_mode.md](references/cognitive_adversary_mode.md)。 @@ -26,6 +26,7 @@ description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 - 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 - 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 +- 以下量化信号至少命中一条时,**强制**走 ROUTE-017(不要求全部满足):当前会话已加载 ≥ 5 份 ref 仍未解决 / 修改涉及 ≥ 3 个独立模块 / 同一问题已跨 ≥ 2 轮对话仍未解决 / 预估代码变更 ≥ 50 行且跨 ≥ 3 个文件。 - 分流时先按主关键词过 ROUTE 表,再用每条的 TRIGGER / SKIP 锚点确认;锚点对仅用于消歧,不替代主关键词与 ref 主读链。 ### 症状导航 @@ -92,7 +93,7 @@ description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - TRIGGER:「搜索预算 / 子代理分流 / 多轮排查策略 / 日志取证预算 / 该用哪个 MCP / MCP 还是裸命令」。 - SKIP:具体排障 → ROUTE-001;具体性能分析 → ROUTE-010。 - [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 - - TRIGGER:「接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构」;同时满足跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地任一升级判据。 + - TRIGGER:「接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构」;满足以下任一条件:(质性)跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地;(量化)会话已加载 ≥ 5 份 ref 仍未解决 / 修改涉及 ≥ 3 个独立模块 / 同一问题已跨 ≥ 2 轮对话仍未解决 / 预估代码变更 ≥ 50 行且跨 ≥ 3 个文件。 - SKIP:单点问题 / 单 ref 即可解决 → 走对应 ROUTE-001~016;仅"问题复杂"或"涉及多个 ref"不算升级条件。 - [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - TRIGGER:「skill / 规则缺失 / 规则冲突 / 验证场景 / 提案 / 自进化」;元工程 / SkillOps 维护任务。 @@ -100,6 +101,21 @@ description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - [ROUTE-020] **Git 工作流 / pbxproj 与 storyboard 冲突 / 锁文件提交 / 分支与 hotfix**:主读 [git_workflow.md](references/git_workflow.md);涉及 PR 拆分与 ownership 追加 [team_collaboration.md](references/team_collaboration.md);涉及 CI / 发布 tag 追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 - TRIGGER:「pbxproj 冲突 / storyboard 合并 / Podfile.lock 或 Package.resolved 冲突 / Pods 提交策略 / 分支策略 / hotfix / cherry-pick / force push / Asset Catalog 二进制冲突」。 - SKIP:仅源码 merge 冲突无 Xcode 工程文件 → 走 ROUTE-015;构建配置 / CI 失败根因 → 走 ROUTE-013;技术债 / PR 拆分通用规则 → 走 ROUTE-015。 +- [ROUTE-021] **Push Notifications / 远程推送 / 本地通知 / 通知服务扩展 / 富媒体通知 / 通知权限**:主读 [notifications.md](references/notifications.md);涉及后台任务追加 [performance_optimization.md](references/performance_optimization.md);涉及证书与签名追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 + - TRIGGER:「推送 / 通知 / UNUserNotificationCenter / APNs / Notification Service Extension / 富媒体通知 / 通知权限 / 静默推送 / provisional authorization」。 + - SKIP:是推送到达后的 UI 渲染问题 → ROUTE-006;是网络重试 / 连接问题 → ROUTE-008。 +- [ROUTE-022] **隐私权限 / 定位 / 相机 / 相册 / 麦克风 / 通讯录 / HealthKit / ATT 追踪 / 权限请求最佳实践**:主读 [privacy_permissions.md](references/privacy_permissions.md);涉及 Info.plist 描述文案追加 [build_release_and_ci.md](references/build_release_and_ci.md);涉及审核拒审风险追加 [migration_strategy.md](references/migration_strategy.md)。 + - TRIGGER:「隐私 / 权限 / 定位 / CLLocationManager / 相机 / 相册 / PHPhotoLibrary / 麦克风 / ATT / AppTrackingTransparency / 权限被拒 / Info.plist 描述 / 审核被拒」。 + - SKIP:是权限获取后对数据的处理逻辑 → 按具体处理类型分流(照片 → ROUTE-006、位置数据建模 → ROUTE-004);是 StoreKit / 内购相关的审核被拒 → ROUTE-024。 +- [ROUTE-023] **SwiftData / Core Data / 持久化 / 数据迁移 / Model Schema / 轻量级迁移 / 重量级迁移**:主读 [persistence.md](references/persistence.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及并发访问追加 [swift_concurrency.md](references/swift_concurrency.md)。 + - TRIGGER:「SwiftData / Core Data / NSPersistentContainer / NSManagedObjectContext / 持久化 / 数据库迁移 / Model Schema 变更 / 轻量级迁移 / 重量级迁移 / @Model / FetchRequest」。 + - SKIP:是内存缓存而非持久化 → ROUTE-008 或 ROUTE-005;是性能问题而非持久化方案 → ROUTE-010。 +- [ROUTE-024] **StoreKit / 内购 / 订阅 / IAP / 收据验证 / 恢复购买 / 促销优惠**:主读 [storekit_iap.md](references/storekit_iap.md);涉及服务端验证追加 [networking_patterns.md](references/networking_patterns.md);涉及审核合规追加 [privacy_permissions.md](references/privacy_permissions.md)。 + - TRIGGER:「StoreKit / 内购 / IAP / 订阅 / 收据验证 / 恢复购买 / 促销优惠 / Product / Transaction / StoreKit 2 / App Store 审核」。 + - SKIP:是支付后的 UI 展示 → ROUTE-005;是 App Store Connect 配置问题 → 提示用户检查 App Store Connect 后台,不在代码层面处理。 +- [ROUTE-025] **App Extensions / Widget / Share Extension / Watch App / Siri Intent / Action Extension / Notification Content Extension**:主读 [app_extensions.md](references/app_extensions.md);涉及跨 Target 数据共享追加 [persistence.md](references/persistence.md);涉及构建配置追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 + - TRIGGER:「Widget / WidgetKit / 小组件 / Share Extension / Watch App / Siri Intent / Action Extension / App Group / 跨 Target 数据共享 / 扩展」。 + - SKIP:是主 App 的 UI / 架构问题 → ROUTE-002 或 ROUTE-006;是构建签名问题 → ROUTE-013。 ## 输出模板 按输出类型触发对应模板,与任务分流正交: diff --git a/skills-engineering/ios-engineer/evolution/approvals/20260705-162659-architecture-expansion-v74.json b/skills-engineering/ios-engineer/evolution/approvals/20260705-162659-architecture-expansion-v74.json new file mode 100644 index 0000000..10b9110 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/approvals/20260705-162659-architecture-expansion-v74.json @@ -0,0 +1,78 @@ +{ + "proposal_id": "20260705-162659-architecture-expansion-v74", + "approved_at": "2026-07-05T17:07:00+0800", + "active_version": "v73", + "validation": { + "structural": true, + "scenario": true, + "risk": "medium" + }, + "changes": [ + { + "file": "skills-engineering/ios-engineer/SKILL.md", + "type": "enhancement", + "description": "IR-001 add exception clauses, ROUTE-017 add quantifiable upgrade signals, add ROUTE-021~025" + }, + { + "file": "skills-engineering/ios-engineer/references/rule_index.md", + "type": "enhancement", + "description": "Add ROUTE-021~025 records, update IR-001 summary" + }, + { + "file": "skills-engineering/ios-engineer/references/self_evolution.md", + "type": "enhancement", + "description": "Add evolution history GC strategy section" + }, + { + "file": "skills-engineering/ios-engineer/references/validation_scenarios.md", + "type": "enhancement", + "description": "Slug list 6→11, add scenarios 7~11" + }, + { + "file": "skills-engineering/ios-engineer/references/notifications.md", + "type": "addition", + "description": "Push Notifications / APNs reference" + }, + { + "file": "skills-engineering/ios-engineer/references/privacy_permissions.md", + "type": "addition", + "description": "Privacy permissions reference" + }, + { + "file": "skills-engineering/ios-engineer/references/persistence.md", + "type": "addition", + "description": "SwiftData / Core Data persistence reference" + }, + { + "file": "skills-engineering/ios-engineer/references/storekit_iap.md", + "type": "addition", + "description": "StoreKit IAP reference" + }, + { + "file": "skills-engineering/ios-engineer/references/app_extensions.md", + "type": "addition", + "description": "App Extensions reference" + }, + { + "file": "skills-engineering/ios-engineer/evolution/scenarios/*.json", + "type": "addition", + "description": "5 new scenario specs (notifications, privacy, persistence, storekit, extensions)" + }, + { + "file": "skills-engineering/ios-engineer/scripts/validate.sh", + "type": "addition", + "description": "Unified validation entry point" + }, + { + "file": "skills-engineering/ios-engineer/scripts/gc_evolution_history.sh", + "type": "addition", + "description": "Snapshot GC script" + }, + { + "file": "skills-engineering/ios-engineer/evolution/hooks/", + "type": "addition", + "description": "Migrated hooks directory" + } + ], + "status": "approved" +} diff --git a/skills-engineering/ios-engineer/hooks/audit-check-on-stop.py b/skills-engineering/ios-engineer/evolution/hooks/audit-check-on-stop.py similarity index 100% rename from skills-engineering/ios-engineer/hooks/audit-check-on-stop.py rename to skills-engineering/ios-engineer/evolution/hooks/audit-check-on-stop.py diff --git a/skills-engineering/ios-engineer/hooks/audit-check-on-stop.sh b/skills-engineering/ios-engineer/evolution/hooks/audit-check-on-stop.sh similarity index 100% rename from skills-engineering/ios-engineer/hooks/audit-check-on-stop.sh rename to skills-engineering/ios-engineer/evolution/hooks/audit-check-on-stop.sh diff --git a/skills-engineering/ios-engineer/hooks/ledger-sync-on-stop.sh b/skills-engineering/ios-engineer/evolution/hooks/ledger-sync-on-stop.sh similarity index 100% rename from skills-engineering/ios-engineer/hooks/ledger-sync-on-stop.sh rename to skills-engineering/ios-engineer/evolution/hooks/ledger-sync-on-stop.sh diff --git a/skills-engineering/ios-engineer/evolution/proposals/20260705-162659-architecture-expansion-v74.md b/skills-engineering/ios-engineer/evolution/proposals/20260705-162659-architecture-expansion-v74.md new file mode 100644 index 0000000..cc64692 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/proposals/20260705-162659-architecture-expansion-v74.md @@ -0,0 +1,52 @@ +# Skill Evolution Proposal + +## Metadata +- Proposal ID: 20260705-162659-architecture-expansion-v74 +- Created At: 2026-07-05 16:26:59 +0800 +- Active Version At Creation: v73 + +## 问题信号 +- 技能树覆盖 5 个 iOS 工程常见领域盲区:Push Notifications(通知扩展/APNs)、隐私权限(定位/相机/ATT)、持久化(SwiftData/Core Data 迁移)、StoreKit 内购、App Extensions(Widget/Share Extension/Watch)。 +- 验证场景仅 6 个(layout/concurrency/review/migration/mcp-control/parameter-pass-through),覆盖不足一半 ROUTE,自进化提案的验证质量依赖 LLM 自评偏差。 +- IR-001"始终使用简体中文"在代码注释/API 名/编译错误/堆栈等场景与工程实际存在张力,缺少例外条款。 +- ROUTE-017 升级判据(跨多日/跨多模块/常规排障无果/需分阶段)依赖 LLM 主观判断,缺少可量化的强制升级信号。 +- evolution/history/ 目录 3248 个文件,每次晋升全量快照无清理策略。 + +## 变更类型 +- 新增能力(ROUTE-021 ~ ROUTE-025 + 5 个新 ref + 4 个新场景) +- 修正表达(IR-001 例外条款、ROUTE-017 量化信号、validation_scenarios.md 场景数文档更新) +- 新增能力(validate.sh 统一验证入口、gc_evolution_history.sh 快照 GC、self_evolution.md GC 策略节) + +## 变更内容 +- 修改文件: + - `SKILL.md`:IR-001 加例外条款、ROUTE-017 加 4 条量化升级信号、新增 ROUTE-021~025 + - `references/rule_index.md`:新增 ROUTE-021~025 记录、更新 IR-001 摘要 + - `references/self_evolution.md`:新增"进化历史 GC 策略"节 + - `references/validation_scenarios.md`:slug 列表 6→11、新增场景 7~11 + - `scripts/validate_scenario_specs.sh`:CANONICAL_SLUGS 6→11 +- 新增文件: + - `references/notifications.md`、`references/privacy_permissions.md`、`references/persistence.md`、`references/storekit_iap.md`、`references/app_extensions.md` + - `evolution/scenarios/notifications.json`、`privacy.json`、`persistence.json`、`storekit.json`、`extensions.json` + - `scripts/validate.sh`(统一验证入口)、`scripts/gc_evolution_history.sh`(快照 GC) +- 替代或合并旧规则:无替代;5 条新 ROUTE 为独立新增能力,不与既有 ROUTE 重叠(TRIGGER/SKIP 已做消歧) + +## 预期收益 +- ROUTE 覆盖从 20 条扩展到 25 条,补齐 Push/隐私/持久化/内购/Extension 五大盲区 +- 验证场景从 6 个扩展到 11 个,覆盖率从 ~33% 提升到 ~44%; +- IR-001 例外条款消除代码输出场景的张力 +- ROUTE-017 量化信号(≥5 ref / ≥3 模块 / ≥2 轮 / ≥50 行跨 ≥3 文件)减少主观判断歧义 +- validate.sh 统一入口减少脚本碎片化维护成本;专项脚本保留为内部子检查 +- GC 策略控制 evolution/history/ 体积 + +## 验证 +- 结构校验:validate.sh --all 全量 13 步 +- 场景回放: + - 新增 5 场景(notifications/privacy/persistence/storekit/extensions)JSON 规格已通过 validate_scenario_specs.sh + - 6 个原有场景无回归 +- 残留风险: + - StoreKit sandbox 真实交易链路仍需后续人工验证;当前自动场景只覆盖客户端规则与验证 fallback 纪律 + - gc_evolution_history.sh 仅干运行测试,未在生产环境确认删除行为 + - 新 ref 的 last-verified 均为 2026-07,需后续定期审计确认 + +## 状态 +- validated diff --git a/skills-engineering/ios-engineer/evolution/scenarios/extensions.json b/skills-engineering/ios-engineer/evolution/scenarios/extensions.json new file mode 100644 index 0000000..2525600 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/scenarios/extensions.json @@ -0,0 +1,47 @@ +{ + "id": "extensions", + "version": 1, + "category": "behavior", + "input": "Widget 刷新偶尔不更新数据,而且点击 Widget 跳转到主 App 时会闪退。Share Extension 也访问不到主 App 的用户 token", + "primary_refs": [ + "references/app_extensions.md" + ], + "output_contract": "four-segment", + "expected_hits": [ + { + "key": "app-group-sharing", + "desc": "识别 Extension 与主 App 的数据共享必须通过 App Group / Keychain Group", + "anchor": "references/app_extensions.md", + "rule_id": "ROUTE-025" + }, + { + "key": "widget-timeline-budget", + "desc": "指出 Widget getTimeline 内的内存和时间 budget,建议主 App 预处理", + "anchor": "references/app_extensions.md" + }, + { + "key": "widget-url-routing", + "desc": "指出 systemSmall widget 仅支持单个 widgetURL,需要防御无效 deep link", + "anchor": "references/app_extensions.md" + } + ], + "failure_signals": [ + { + "key": "direct-access-main-app", + "desc": "建议 Extension 直接访问主 App 沙盒目录或 UserDefaults.standard" + }, + { + "key": "heavy-work-in-timeline", + "desc": "建议在 getTimeline 内做重请求或复杂 image processing" + }, + { + "key": "no-extension-lifecycle", + "desc": "忽略 Extension 独立进程、内存受限、随时可能被终止的事实" + } + ], + "scoring": { + "pass": "所有 expected_hits 命中且无 failure_signals 触发", + "partial": "至少半数 expected_hits 命中且未触发任何核心 failure_signals", + "fail": "命中任一 failure_signals 或 expected_hits 命中不到一半" + } +} diff --git a/skills-engineering/ios-engineer/evolution/scenarios/notifications.json b/skills-engineering/ios-engineer/evolution/scenarios/notifications.json new file mode 100644 index 0000000..5733f7d --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/scenarios/notifications.json @@ -0,0 +1,48 @@ +{ + "id": "notifications", + "version": 1, + "category": "behavior", + "input": "推送到达后通知扩展里下载图片偶尔失败,而且用户点击通知跳转的页面不对,帮我排查", + "primary_refs": [ + "references/notifications.md" + ], + "output_contract": "four-segment", + "expected_hits": [ + { + "key": "extension-memory-limit", + "desc": "识别 Notification Service Extension 内存限制(~24MB)和 30 秒超时", + "anchor": "references/notifications.md", + "rule_id": "ROUTE-021" + }, + { + "key": "four-segment-output", + "desc": "输出保持根因 / 为什么 / 修法 / 验证", + "anchor": "SKILL.md:12", + "rule_id": "GR-004" + }, + { + "key": "extension-lifecycle-awareness", + "desc": "指出 Extension 只有配置相同 Keychain Access Group 才能访问共享 Keychain item,且不应在 Extension 内发起长时网络请求", + "anchor": "references/notifications.md" + } + ], + "failure_signals": [ + { + "key": "ignore-extension-constraints", + "desc": "未考虑 Extension 的内存/时间/沙盒限制,直接建议在 Extension 内做重操作" + }, + { + "key": "push-to-ui-routing", + "desc": "跳过推送到达 → 用户点击 → 路由跳转的链路分析" + }, + { + "key": "hardcoded-routing", + "desc": "建议在 AppDelegate 内硬编码通知路由映射" + } + ], + "scoring": { + "pass": "所有 expected_hits 命中且无 failure_signals 触发", + "partial": "至少半数 expected_hits 命中且未触发任何核心 failure_signals", + "fail": "命中任一 failure_signals 或 expected_hits 命中不到一半" + } +} diff --git a/skills-engineering/ios-engineer/evolution/scenarios/persistence.json b/skills-engineering/ios-engineer/evolution/scenarios/persistence.json new file mode 100644 index 0000000..8bc9733 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/scenarios/persistence.json @@ -0,0 +1,47 @@ +{ + "id": "persistence", + "version": 1, + "category": "behavior", + "input": "Core Data 加了新字段后迁移失败,数据丢了。现在想迁到 SwiftData,但不知道能不能平滑过渡", + "primary_refs": [ + "references/persistence.md" + ], + "output_contract": "four-segment", + "expected_hits": [ + { + "key": "migration-strategy", + "desc": "区分轻量级迁移和重量级迁移场景,给出对应策略", + "anchor": "references/persistence.md", + "rule_id": "ROUTE-023" + }, + { + "key": "backup-before-migration", + "desc": "迁移前必须备份数据库,失败时不清空数据", + "anchor": "references/persistence.md" + }, + { + "key": "context-threading", + "desc": "指出 Core Data context 的线程模型约束", + "anchor": "references/persistence.md" + } + ], + "failure_signals": [ + { + "key": "migrate-without-backup", + "desc": "建议直接清空 persistent store 重建而不备份" + }, + { + "key": "cross-context-usage", + "desc": "建议跨 context 传递 NSManagedObject" + }, + { + "key": "mixed-frc-swiftdata", + "desc": "在 SwiftData 方案中建议混用 NSFetchedResultsController" + } + ], + "scoring": { + "pass": "所有 expected_hits 命中且无 failure_signals 触发", + "partial": "至少半数 expected_hits 命中且未触发任何核心 failure_signals", + "fail": "命中任一 failure_signals 或 expected_hits 命中不到一半" + } +} diff --git a/skills-engineering/ios-engineer/evolution/scenarios/privacy.json b/skills-engineering/ios-engineer/evolution/scenarios/privacy.json new file mode 100644 index 0000000..4df9183 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/scenarios/privacy.json @@ -0,0 +1,47 @@ +{ + "id": "privacy", + "version": 1, + "category": "behavior", + "input": "App 首次启动请求相机权限,用户拒绝后功能不可用,也没有引导去设置。另外 ATT 弹窗时机不对,审核被拒了", + "primary_refs": [ + "references/privacy_permissions.md" + ], + "output_contract": "four-segment", + "expected_hits": [ + { + "key": "permission-timing", + "desc": "指出权限必须在用户明确行为上下文内请求,不可在启动时批量弹", + "anchor": "references/privacy_permissions.md", + "rule_id": "ROUTE-022" + }, + { + "key": "denied-degradation", + "desc": "给出权限被拒后的降级路径:禁用按钮 / 引导文案 / 跳转设置", + "anchor": "references/privacy_permissions.md" + }, + { + "key": "att-pre-permission", + "desc": "ATT 弹窗前需要 pre-permission 说明弹窗", + "anchor": "references/privacy_permissions.md" + } + ], + "failure_signals": [ + { + "key": "no-plist-check", + "desc": "未检查 Info.plist 中 *UsageDescription key 是否存在" + }, + { + "key": "reattempt-denied", + "desc": "建议在 denied 状态下重试弹出系统权限弹窗(无效操作)" + }, + { + "key": "no-app-review-risk", + "desc": "未提及审核拒审风险" + } + ], + "scoring": { + "pass": "所有 expected_hits 命中且无 failure_signals 触发", + "partial": "至少半数 expected_hits 命中且未触发任何核心 failure_signals", + "fail": "命中任一 failure_signals 或 expected_hits 命中不到一半" + } +} diff --git a/skills-engineering/ios-engineer/evolution/scenarios/storekit.json b/skills-engineering/ios-engineer/evolution/scenarios/storekit.json new file mode 100644 index 0000000..50f965d --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/scenarios/storekit.json @@ -0,0 +1,47 @@ +{ + "id": "storekit", + "version": 1, + "category": "behavior", + "input": "订阅购买后偶尔不到账,恢复购买也不稳定。现在客户端把到期时间存在 UserDefaults,服务端验证失败就切 sandbox 重试", + "primary_refs": [ + "references/storekit_iap.md" + ], + "output_contract": "four-segment", + "expected_hits": [ + { + "key": "entitlement-source", + "desc": "指出购买状态不能只依赖 UserDefaults,必须以 StoreKit 交易或服务端验证结果为准", + "anchor": "references/storekit_iap.md", + "rule_id": "ROUTE-024" + }, + { + "key": "transaction-listener", + "desc": "指出 Transaction.updates 或 SKPaymentTransactionObserver 需要在 App 生命周期内持续监听", + "anchor": "references/storekit_iap.md" + }, + { + "key": "sandbox-fallback-guard", + "desc": "指出 production 到 sandbox 的 fallback 只能在明确 sandbox receipt 指示时发生,不能吞掉所有生产验证失败", + "anchor": "references/storekit_iap.md" + } + ], + "failure_signals": [ + { + "key": "local-cache-entitlement", + "desc": "建议用本地缓存直接判定订阅权益" + }, + { + "key": "restore-on-launch", + "desc": "建议每次启动都调用 AppStore.sync 或 restoreCompletedTransactions" + }, + { + "key": "unconditional-sandbox-fallback", + "desc": "把所有服务端验证失败都 fallback 到 sandbox" + } + ], + "scoring": { + "pass": "所有 expected_hits 命中且无 failure_signals 触发", + "partial": "至少半数 expected_hits 命中且未触发任何核心 failure_signals", + "fail": "命中任一 failure_signals 或 expected_hits 命中不到一半" + } +} diff --git a/skills-engineering/ios-engineer/references/app_extensions.md b/skills-engineering/ios-engineer/references/app_extensions.md new file mode 100644 index 0000000..ac7c0e6 --- /dev/null +++ b/skills-engineering/ios-engineer/references/app_extensions.md @@ -0,0 +1,46 @@ + +# App Extensions 工程规范 + +## 使用规则 +- 涉及 Widget(WidgetKit)、Share Extension、Watch App、Siri Intent、Notification Content Extension、Action Extension 时必须使用本文件。 +- Extension 是独立进程,不与主 App 共享内存空间;数据共享必须通过 App Group 或 Keychain Group。 +- 默认输出"类型选型 → 数据共享 → 生命周期 → 构建配置 → 验证"五段。 + +## Widget(WidgetKit / iOS 14+) +- 使用 `TimelineProvider` 驱动刷新:`snapshot`(预览)→ `timeline`(真实数据)→ `placeholder`(占位)。 +- 时间线条目 `TimelineEntry` 的 `date` 字段决定何时显示、何时刷新;超过 `Timeline` 的 `policy` 过期时间后系统会请求新数据。 +- `WidgetFamily`(systemSmall / systemMedium / systemLarge / accessory*)决定展示尺寸和内容区域;每个 family 都必须有对应视图。 +- 网络请求在 `getTimeline` 内发起,必须在 Extension 的内存 budget 内完成(约 30MB iOS 17+);不得发起连续重试。 +- 与主 App 通信:通过 `UserDefaults(suiteName:)`(App Group)共享轻量数据;大数据或结构化数据建议通过共享 container 的文件 URL。 +- 点击 Widget 打开主 App:`widgetURL(_:)` 或 `Link(destination:)` 设置 deep link;`systemSmall` 仅支持单个 `widgetURL`,多目标需用 `systemMedium` 或 `systemLarge`。 + +## Share Extension +- 接收 `NSExtensionItem` 数组;附件类型为 `NSItemProvider`,支持文本、URL、图片、视频等。 +- 必须通过 App Group 共享 UserDefaults / 文件 URL 与主 App 传递数据;不可直接访问主 App 的沙盒目录。 +- 生命周期:用户点击"分享"后打开 Extension 视图 → 用户完成操作(post / save)后 Extension 关闭;期间不被 Suspended。 +- UI 不可过重——Extension 内存限制严格(~120MB),且用户在完成操作前不可退出。 + +## Watch App +- watchOS App 运行在独立进程;与 iPhone 通信通过 `WCSession`(WatchConnectivity)。 +- `WCSession.sendMessage(_:replyHandler:errorHandler:)` 是实时通信方式(仅当 watch 和 iPhone 都在前台);`transferUserInfo(_:)` / `updateApplicationContext(_:)` 用于后台同步。 +- Watch 上的持久化独立于 iPhone;需同步的数据通过 WCSession + 共享 container 协调。 +- Watch App 的性能指标严苛:前端交互延迟 < 200ms,内存上限极小(根据型号 ~60-120MB)。 + +## 跨 Target 数据共享 +| 共享方式 | 适用场景 | 限制 | +|---------|---------|------| +| App Group UserDefaults | 简单键值对(token、配置开关) | 不保证实时同步;大小有限 | +| App Group Container URL | 大文件、数据库文件 | 需手动管理并发访问 | +| Keychain Group | 敏感凭证(token、密码) | 需在 entitlements 中配置 | +| Darwin Notification | 跨进程轻量信号 | 不携带 payload;不可靠(best-effort) | + +## 构建配置 +- 每个 Extension Target 必须单独配置 Provisioning Profile 和 Bundle ID(通常为 `com.example.app.widget`)。 +- Debug 构建时需选择正确的 Scheme(主 App vs Extension);Extension 不可独立运行。 +- App Group capability 必须在主 App 和 Extension 的 entitlements 中同时开启且 group identifier 一致。 + +## 常见反模式 +- 在主 App 的 `viewDidLoad` 中假定 App Group UserDefaults 已被 Extension 写入——Extension 可能尚未运行。 +- Widget `getTimeline` 中进行重请求和复杂 image processing——应在主 App 中预处理并通过 App Group 共享。 +- Share Extension 中通过 Keychain 直接读主 App 的 token(未配置 Keychain Group)——Extension 不可访问主 App 的 Keychain。 +- Widget 刷新间隔设太短(< 5 分钟)——系统会节流刷新频率。 diff --git a/skills-engineering/ios-engineer/references/notifications.md b/skills-engineering/ios-engineer/references/notifications.md new file mode 100644 index 0000000..e369981 --- /dev/null +++ b/skills-engineering/ios-engineer/references/notifications.md @@ -0,0 +1,41 @@ + +# Push Notifications 工程规范 + +## 使用规则 +- 涉及远程推送(APNs)、本地通知(UNUserNotificationCenter)、通知扩展时必须使用本文件。 +- 推送设计应区分通知到达、用户点击、前台展示三条链路,不可混为一谈。 +- 默认输出"注册链 → payload 结构 → 路由跳转 → 扩展处理"四段。 + +## 通知注册链 +- `UNUserNotificationCenter.requestAuthorization(options:)` 必须提供 `.alert` / `.badge` / `.sound` 的最小组合;provisional 授权用于软开通,不可替代正式授权。 +- 注册失败不得静默:记录错误码并给出用户可见的降级路径(如设置页引导)。 +- 主 Target 注册 token 通过 `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` 获取;token 变更时必须重新上报服务端。 +- App 冷启动时 `didFinishLaunching` 内如果未调用 `registerForRemoteNotifications()`,token 可能过期而无法刷新。 + +## Payload 结构 +- APNs payload 顶层 `aps` 字典为系统保留字段;业务数据放在自定义 key 下,不与 `aps` 同级混放(避免未来字段冲突)。 +- `mutable-content: 1` + Notification Service Extension 允许富媒体附件处理(图片 / 视频 / 音频),但处理超时约 30 秒,必须在 `didReceive(_:withContentHandler:)` 内及时调用 `contentHandler`。 +- 静默推送(`content-available: 1`)不保证送达时序,不可依赖其顺序编排业务逻辑。 + +## 路由跳转 +- 通知点击跳转不应在 `AppDelegate` / `SceneDelegate` 内硬编码路由映射;建议通过通知 payload 中的 `route` / `deepLink` 字段驱动导航。 +- 用户点击历史通知时可能指向已卸载的内容,路由处理须防御无效 deep link。 +- 通知送达时若 App 在前台,默认不展示横幅;需要 `UNUserNotificationCenterDelegate.presentationOptions` 返回 `.banner` / `.list` / `.sound` / `.badge`。 + +## 通知扩展 +- Notification Service Extension 运行在独立进程;共享文件或 `UserDefaults(suiteName:)` 需要 App Group,访问共享 Keychain item 需要主 App 与 Extension 配置相同 Keychain Access Group,不可把 App Group 当成 Keychain 共享前提。 +- Extension 内存受限(~24MB iOS 15+),处理大图或视频时应优先传 URL 而非 raw data。 +- Notification Content Extension 用于自定义通知详情展开后的 UI;其生命周期由系统管理,不可在其中发起长时网络请求。 + +## 常见反模式 +- 在 `didFinishLaunching` 中仅因 `launchOptions[.remoteNotification] != nil` 就判定用户来自通知点击——`launchOptions` 存在不代表用户看到了通知内容,应同时检查对应 key 的 payload 是否完整。 +- 将 token 上报与推送策略耦合——token 只是地址,推送策略(时间 / 频控 / 分段)应由服务端独立管理。 +- 在 Extension 内使用 `shared` URLSession 进行大文件下载——Extension 随时可能被系统终止,下载应在 Extension 内最小化,大文件应由主 App 后台下载。 + +## 验证清单 +- [ ] 通知注册成功/失败均有日志与降级路径。 +- [ ] token 变更后服务端在 5 分钟内生效。 +- [ ] 富媒体推送(mutable-content)在 Extension 内 30 秒内完成处理。 +- [ ] 通知点击路由可处理无效/过期的 deep link。 +- [ ] 前台通知展示行为符合预期(横幅/不横幅)。 +- [ ] Extension 崩溃率 < 0.1%。 diff --git a/skills-engineering/ios-engineer/references/persistence.md b/skills-engineering/ios-engineer/references/persistence.md new file mode 100644 index 0000000..b3a5b8a --- /dev/null +++ b/skills-engineering/ios-engineer/references/persistence.md @@ -0,0 +1,38 @@ + +# 持久化工程规范(SwiftData / Core Data) + +## 使用规则 +- 涉及 SwiftData、Core Data、数据持久化、Model Schema、迁移策略时必须使用本文件。 +- 新技术栈(iOS 17+)优先选 SwiftData;旧项目或需兼容 iOS 16 及以下选 Core Data + NSPersistentContainer。 +- 默认输出"技术选型 → Schema 设计 → 并发模型 → 迁移策略 → 验证"五段。 + +## SwiftData(iOS 17+) +- 使用 `@Model` 宏标注持久化模型;自动生成 `PersistentModel` 符合。 +- 默认存储位置:`ModelContainer` 不指定 URL 时存储在 App Group / Application Support 下。 +- 与 CloudKit 集成:`ModelConfiguration(cloudKitContainerIdentifier:)` 自动启用 NSPersistentCloudKitContainer 后端。 +- `@Query` / `@Transient` / `@Attribute(.unique)` / `@Relationship` 等宏提供声明式约束。 +- 局限性:不支持 `NSFetchedResultsController` 层级缓存;大结果集分页需配合 `FetchDescriptor.fetchLimit` + `fetchOffset`;批量操作(`NSBatchDeleteRequest` / `NSBatchUpdateRequest`)需回退到 Core Data API。 + +## Core Data(通用) +- 必须使用 `NSPersistentContainer`(iOS 10+),不得手动构造 `NSManagedObjectModel` / `NSPersistentStoreCoordinator` / `NSManagedObjectContext` 三层。 +- `viewContext` 绑定主队列;写操作使用 `performBackgroundTask` 或 `newBackgroundContext()`。 +- `NSManagedObject` 不跨 context 传递:在 context A 中获取的对象不能直接在 context B 中使用;必须通过 `objectID` 在目标 context 中重新 fetch。 +- `NSFetchedResultsController` 用于列表场景的增量刷新;delegate 回调在主线程,内部不应做重计算。 + +## Schema 迁移 +- 轻量级迁移(Lightweight Migration):仅修改属性名、类型(兼容转换)、增加/删除可选属性时可自动处理;在 `NSPersistentStoreDescription` 中设置 `shouldMigrateStoreAutomatically = true` + `shouldInferMappingModelAutomatically = true`。 +- 重量级迁移(Heavyweight Migration):涉及实体拆分/合并、关系重构、属性类型不兼容变更时,必须提供 `NSMappingModel` 或使用渐进式迁移(多版本链)。 +- SwiftData 迁移:通过 `Schema` 和 `VersionedSchema` 定义版本链;`ModelContainer` 自动在版本间迁移,但复杂迁移仍需介入。 +- 迁移前必须备份数据库文件;迁移失败时不得清空数据,应提示用户并保留原文件。 + +## 并发模型 +- Core Data:`viewContext`(主队列并发类型)用于 UI 读取;`performBackgroundTask` 创建的私有 context 用于写操作;context 间通过 `objectID` 传递对象引用。 +- SwiftData:`@MainActor ModelContext` 用于 UI;通过 `ModelActor` 或显式 `Task { @MainActor in }` 处理异步持久化。 +- 批量操作(`NSBatchDeleteRequest` / `NSBatchUpdateRequest`)绕过 context 和内存中的对象,执行后必须刷新相关 context(`mergeChanges` 或重建)。 +- 不得在 `viewContext` 的 `perform` 闭包内执行同步网络请求——会阻塞主队列。 + +## 常见反模式 +- 将 `NSManagedObject` 跨线程传递或存储为属性——使用 `objectID`。 +- 迁移时不清空 persistent store 直接重建——迁移失败时数据不可逆。 +- 在 `viewContext` 中执行长时间写操作——写操作始终在后台 context。 +- SwiftData 中混用 `NSFetchedResultsController`——SwiftData 使用 `@Query` 的 Observation 机制,不兼容 FRC。 diff --git a/skills-engineering/ios-engineer/references/privacy_permissions.md b/skills-engineering/ios-engineer/references/privacy_permissions.md new file mode 100644 index 0000000..4bf1bdf --- /dev/null +++ b/skills-engineering/ios-engineer/references/privacy_permissions.md @@ -0,0 +1,44 @@ + +# 隐私权限工程规范 + +## 使用规则 +- 涉及定位、相机、相册、麦克风、通讯录、HealthKit、ATT 追踪、本地网络等受保护权限时必须使用本文件。 +- 每个权限必须补充对应的 `Info.plist` 描述文案(`*UsageDescription` key),缺漏将导致审核拒绝或运行时 crash。 +- 默认输出"权限类型 → 请求时机 → 拒绝降级 → plist 文案 → 审核风险"五段。 + +## 权限请求最佳实践 +- 权限请求必须发生在用户明确行为上下文内(如点击"拍照"按钮时请求相机),禁止在 App 启动时批量弹权限。 +- 权限被拒后的降级路径必须可见:禁用按钮、显示引导文案、提供跳转系统设置的入口。 +- iOS 权限请求只有 `notDetermined` 状态会触发系统弹窗;`denied` / `restricted` 不应重复请求系统弹窗,必须走可见降级与设置页引导;相册 `limited` 需要单独提供受限访问下的功能路径。 +- 定位权限分为 `When In Use` 和 `Always`:先申请 `When In Use`,通过后再申请 `Always`;直接申请 `Always` 极大概率被用户拒绝。 + +## 必备案底(Info.plist) +| 权限类型 | plist Key | 必填描述示例 | +|---------|-----------|------------| +| 定位 Always | `NSLocationAlwaysAndWhenInUseUsageDescription` | 用于持续记录您的行程轨迹 | +| 定位 WhenInUse | `NSLocationWhenInUseUsageDescription` | 用于在地图上显示您的当前位置 | +| 相机 | `NSCameraUsageDescription` | 用于拍照识别 / 扫码 | +| 相册读取 | `NSPhotoLibraryUsageDescription` | 用于选择照片上传 | +| 相册写入 | `NSPhotoLibraryAddUsageDescription` | 用于保存图片到相册 | +| 麦克风 | `NSMicrophoneUsageDescription` | 用于语音消息录制 | +| 通讯录 | `NSContactsUsageDescription` | 用于邀请好友 | +| ATT 追踪 | `NSUserTrackingUsageDescription` | 用于为您提供个性化广告 | +| 蓝牙 | `NSBluetoothAlwaysUsageDescription` | 用于连接智能设备 | + +## ATT 追踪(AppTrackingTransparency) +- iOS 14.5+ 必须通过 `ATTrackingManager.requestTrackingAuthorization` 获取用户授权后才能获取 IDFA。 +- ATT 弹窗只允许出现一次系统弹窗(苹果限制);如果 dismissed 后想再弹,必须从系统设置的对应 app 页面手动操作。 +- ATT 状态为 `notDetermined` 时调用 `requestTrackingAuthorization` 会触发系统弹窗;`denied` 状态下再次调用不会触发弹窗(直接返回 denied),不应依赖弹窗覆盖来重新申请。 +- 建议:在 ATT 弹窗前先弹一个 pre-permission 说明弹窗,告知用户为什么需要追踪,提升授权率。 + +## 审核拒审风险 +- 缺少对应的 `*UsageDescription` key 会导致访问隐私 API 时运行时崩溃。 +- 文案与实际用途不符(如声称"用于导航"但实际用于广告)会被审核拒绝或下架。 +- 请求权限但不在应用中实际使用(被静态分析检测到调用无后续使用)会触发审核。 +- 权限处于 `restricted` / MDM / 家长控制等用户不可自行修改状态时,仍反复引导去设置可能导致审核 flag。 + +## 常见反模式 +- 在 `viewDidLoad` 或 `init` 中同步请求权限——必须响应用户行为。 +- 使用被拒绝权限的功能时直接 assert / fatalError——必须给出降级路径。 +- ATT 弹窗在启动时直接弹出而无 pre-permission 说明——审核可拒。 +- 缺少 `NSLocationWhenInUseUsageDescription` 而只配置了 `Always`——定位功能直接不可用。 diff --git a/skills-engineering/ios-engineer/references/rule_index.md b/skills-engineering/ios-engineer/references/rule_index.md index c75f43c..045e7fa 100644 --- a/skills-engineering/ios-engineer/references/rule_index.md +++ b/skills-engineering/ios-engineer/references/rule_index.md @@ -18,7 +18,7 @@ | ID | Status | 摘要 | SKILL.md 锚点 | |----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | +| IR-001 | active | 始终使用简体中文;代码注释/API 名/编译错误/崩溃堆栈/命令输出/日志字面值可保留原文;对话/方案/诊断/规则输出仍强制中文 | `## 核心铁律` | | IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,"结论"前必须有独立"版本前提"块(真值或显式假设),字段存在性可机械校验 | 同上 | | IR-011 | active | 命中认知对手模式时必须输出复述/最强反驳/隐藏假设/失效条件/可证伪条件/立场翻转/迎合自检/置信度/结论 | 同上 | @@ -75,6 +75,11 @@ GR-NNN 规则由独立 global skill 承载,跨平台通用(不限 iOS);i | ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | | ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | | ROUTE-020 | active | Git 工作流 / pbxproj 与 storyboard 冲突 / 锁文件提交 / 分支与 hotfix → git_workflow.md | 同上 | +| ROUTE-021 | active | Push Notifications / 远程推送 / 本地通知 / 通知服务扩展 / 富媒体通知 / 通知权限 → notifications.md | 同上 | +| ROUTE-022 | active | 隐私权限 / 定位 / 相机 / 相册 / 麦克风 / 通讯录 / HealthKit / ATT 追踪 / 权限请求 → privacy_permissions.md | 同上 | +| ROUTE-023 | active | SwiftData / Core Data / 持久化 / 数据迁移 / Model Schema / 轻量级迁移 / 重量级迁移 → persistence.md | 同上 | +| ROUTE-024 | active | StoreKit / 内购 / 订阅 / IAP / 收据验证 / 恢复购买 / 促销优惠 → storekit_iap.md | 同上 | +| ROUTE-025 | active | App Extensions / Widget / Share Extension / Watch App / Siri Intent / Action Extension / Notification Content Extension → app_extensions.md | 同上 | ## 输出模板 OUT-NNN diff --git a/skills-engineering/ios-engineer/references/self_evolution.md b/skills-engineering/ios-engineer/references/self_evolution.md index f72870c..5b7e944 100644 --- a/skills-engineering/ios-engineer/references/self_evolution.md +++ b/skills-engineering/ios-engineer/references/self_evolution.md @@ -55,8 +55,9 @@ 4. 运行验证 - 至少执行结构校验、引用校验和场景校验。 - 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 +- 对外统一入口使用 [scripts/validate.sh](../scripts/validate.sh):`--all` 跑完整门禁,`--quick` 跑快速结构校验,`--scenarios` 跑场景规格和内部链接校验;`validate_skill_evolution.sh` / `validate_scenario_specs.sh` / `validate_rule_ids.sh` / `validate_usage_ledger.sh` 保留为内部子检查或专项排障入口。 - 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 +- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在固定 slug 内,否则后续 grader 无法对账。 - 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 5. 通过后再晋升 @@ -84,7 +85,7 @@ - 命中的验证场景没有回归。 建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 +- 运行 [scripts/validate.sh](../scripts/validate.sh) `--all` 做完整门禁;本地快速检查可用 `--quick`,只验证场景规格可用 `--scenarios`。 - 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 - 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 - 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 @@ -108,7 +109,7 @@ ## 真实任务观测 - 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 - 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 +- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在统一校验的 `[8/14]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 - ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 - 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 @@ -161,3 +162,32 @@ - 脚本对 `CRITICAL` / `UNDATED` / `INVALID` 任一非零即非零退出,便于接入 CI 或定时检查; - 阈值可按需用环境变量覆盖; - STALE / CRITICAL ref 应优先进入"## 触发信号"列表的最后一条,开新提案做内容复核或退役判定。 + +## 进化历史 GC 策略 + +`evolution/history/` 每次晋升产生全量快照,随版本积累会快速膨胀。以下策略控制目录体积: + +**保留规则**: +- 始终保留最近 10 个版本的完整快照。 +- 每 10 个版本(v10, v20, v30...)保留一个里程碑快照作为长期还原点。 +- 其他版本的快照在晋升下一个版本后自动清理。 + +**清理脚本**: +```bash +# 示例:仅保留最近 10 版 + 每 10 版里程碑 +bash scripts/gc_evolution_history.sh +``` + +**清理触发时机**: +- 每次新版本晋升成功后自动触发 GC(在 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 末尾调用)。 +- 也可手动运行(不会删除当前 active 版本及最近 10 版的快照)。 +- 如需临时跳过自动清理,可设置 `SKIP_EVOLUTION_GC=1` 后再执行晋升脚本;跳过后应手动运行一次 GC。 + +**受保护快照(永不删除)**: +- `active_version.json` 指向的当前版本快照。 +- 里程碑版本快照(版本号能被 10 整除且 ≥ v10)。 +- 最近 10 个版本的快照。 + +**干运行模式**: +- `gc_evolution_history.sh --dry-run` 仅列出将被删除的目录,不实际删除。 +- 首次部署建议先干运行确认列表。 diff --git a/skills-engineering/ios-engineer/references/storekit_iap.md b/skills-engineering/ios-engineer/references/storekit_iap.md new file mode 100644 index 0000000..a6a9bcf --- /dev/null +++ b/skills-engineering/ios-engineer/references/storekit_iap.md @@ -0,0 +1,40 @@ + +# StoreKit / 内购工程规范 + +## 使用规则 +- 涉及 StoreKit 2(iOS 15+)、StoreKit 1(legacy)、内购商品、订阅、收据验证、促销优惠时必须使用本文件。 +- iOS 15+ 优先使用 StoreKit 2 的 `Product` / `Transaction` / `Transaction.updates` API;需支持 iOS 14 时回退 StoreKit 1(`SKPaymentQueue` 体系)。 +- 默认输出"商品获取 → 购买流程 → 收据验证 → 恢复与同步 → 订阅管理"五段。 + +## StoreKit 2(iOS 15+ 推荐) +- 商品加载:`Product.products(for:)` 异步返回 `[Product]`;需处理网络失败与空结果(用户无购买权限或地区不可用)。 +- 购买:通过 `product.purchase()` 返回 `Product.PurchaseResult`;`.success(.verified(transaction))` 表示购买成功且通过验证;`.success(.unverified(_,_))` 表示购买存在但签名验证失败(需手动处理)。 +- 交易监听:`Transaction.updates` 是一个 `AsyncSequence`,在 App 生命周期内监听新交易(包括跨设备同步和订阅续期);必须在 App 启动时开始监听并持续运行。 +- 收据验证:StoreKit 2 通过 `Transaction.currentEntitlements` 获取已验证的交易;服务端验证可选 `AppTransaction` / `Transaction` 的 JWS 签名(通过 Apple 验证端点在线验证)。 +- 恢复购买:`AppStore.sync()` 同步跨设备交易,返回之前未在该设备上完成的交易;不应在每次启动时调用——按需触发。 + +## StoreKit 1(iOS 14 及以下兼容) +- 使用 `SKProductsRequest` 获取商品信息(delegate 模式)。 +- 使用 `SKPaymentQueue.default().add(payment)` 发起购买;通过 `SKPaymentTransactionObserver` 监听交易状态变化。 +- 收据验证:通过 `Bundle.main.appStoreReceiptURL` 获取收据文件,base64 编码后发送服务端验证。 +- 重要:`application(_:didFinishLaunchingWithOptions:)` 中开始监听 `SKPaymentQueue`;忘记添加 observer 会导致购买回调丢失。 + +## 收据验证双路径 +| 路径 | 适用场景 | 优点 | 风险 | +|------|---------|------|------| +| 设备端验证 | 非消耗型 / 自动续期订阅的简单判断 | 离线可用,延迟低 | 容易被越狱绕过 | +| 服务端验证 | 消耗型商品 / 订阅 / 敏感权益 | 安全,Apple 权威 | 增加网络延迟,需处理验证明文超时 | + +服务端验证优先级:legacy receipt 校验只有在 production 端点明确返回 sandbox receipt 指示时才 fallback 到 sandbox;其他 production 验证失败必须按网络错误、签名错误、状态码错误或服务端异常分别处理,不可一概吞掉后重试 sandbox;不可在代码中硬编码验证 URL。 + +## 订阅管理 +- 订阅状态通过 `Transaction.currentEntitlements`(StoreKit 2)或收据解析(StoreKit 1)判定;不可仅依赖 `UserDefaults` 中缓存的过期时间。 +- 促销优惠(Promotional Offers):在 App Store Connect 配置后,通过 `paymentQueue(_:shouldAddStorePayment:for:)` 处理。 +- 订阅优惠码 / 推介促销:StoreKit 2 通过 `Product.SubscriptionInfo.PromotionalOffer` 处理。 +- 必须显示管理订阅的人口(`AppStore.showManageSubscriptions(in:)` iOS 15+ 或打开 `itms-apps://` 链接)。 + +## 常见反模式 +- 用 `UserDefaults` 存储购买状态而不验证收据——极易被破解。 +- 每次启动都调用 `AppStore.sync()` / `restoreCompletedTransactions()`——浪费 Apple 服务器资源且有 rate limiting。 +- 仅在 App 前台监听 `Transaction.updates`——App 从后台回到前台时需要检查漏掉的交易。 +- 购买流程中不用 loading 状态阻塞用户——用户可能多次点击导致重复购买(多次扣款)。 diff --git a/skills-engineering/ios-engineer/references/usage_ledger.md b/skills-engineering/ios-engineer/references/usage_ledger.md index f09edc9..f8ab55e 100644 --- a/skills-engineering/ios-engineer/references/usage_ledger.md +++ b/skills-engineering/ios-engineer/references/usage_ledger.md @@ -30,7 +30,7 @@ | `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | | `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | | `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | +| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `notifications` / `privacy` / `persistence` / `storekit` / `extensions` / `other` | | `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `GR-005`) | | `hit_rules` | string[] | 是 | 同上;可为空数组 | | `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | @@ -95,7 +95,7 @@ evolution-signal: 修正表达 ## 5. 三端 system-prompt 片段(可粘贴) -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 +三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 11 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 ### 5.1 Codex CLI @@ -104,11 +104,12 @@ evolution-signal: 修正表达 ``` ## ios-engineer skill audit 当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} +{layout, parameter-pass-through, concurrency, review, migration, mcp-control, +notifications, privacy, persistence, storekit, extensions, other} 之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill references/usage_ledger.md 第 4 节): - tool: codex -- task-type: 上述 7 选 1 +- task-type: 上述 12 选 1 - prompt-summary: 5-200 字符脱敏摘要 - expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, 来源是 ios-engineer/references/rule_index.md 的 active 集合 @@ -128,7 +129,8 @@ references/usage_ledger.md 第 4 节): 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 - tool: claude-code - task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} + migration, mcp-control, notifications, privacy, persistence, storekit, + extensions, other} - expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 status=active 的 ID - 不确定 hit-rules 时留空,不要凭印象猜测 @@ -146,7 +148,7 @@ references/usage_ledger.md 第 4 节): 格式见 ios-engineer/references/usage_ledger.md 第 4 节。 - tool: cursor - task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} + mcp-control, notifications, privacy, persistence, storekit, extensions, other} - expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX - 不确定就留空,不猜 - prompt-summary 5-200 字符脱敏 @@ -184,7 +186,7 @@ Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 se | 常量 | 值 | 候选提案信号 | 含义 | |------|----|-------------|------| | `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | +| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 11 个 slug 覆盖不全,可能需新增场景 | | `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | | `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | diff --git a/skills-engineering/ios-engineer/references/validation_scenarios.md b/skills-engineering/ios-engineer/references/validation_scenarios.md index ebe73d0..5b6fcf7 100644 --- a/skills-engineering/ios-engineer/references/validation_scenarios.md +++ b/skills-engineering/ios-engineer/references/validation_scenarios.md @@ -5,16 +5,16 @@ - 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 - 每次验证只测 1 个场景,不把多个场景混在一轮。 - 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 +- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`、`notifications`、`privacy`、`persistence`、`storekit`、`extensions`。 +- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 11 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;统一入口 [scripts/validate.sh](../scripts/validate.sh) `--scenarios` 会断言两侧 slug 一致、字段齐全,并检查内部链接。 ### JSON 与本文同步流程 按以下顺序执行,跳步会被对应脚本捕获: 1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 +2. 跑 [scripts/validate.sh](../scripts/validate.sh) `--scenarios` — 断言 11 份 JSON 与本文 slug 双向一致、字段齐全,并检查内部链接。漏跑会让 slug 漂移在 grader 阶段才暴露。 3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 +4. 跑 [scripts/validate.sh](../scripts/validate.sh) `--ids` — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 ## 验证目标 - 输出是否优先给出最可能根因,而不是铺开多个大分支。 @@ -124,6 +124,86 @@ review 这个改动,重点看有没有隐藏回归。 - 没有预算意识。 - 同一方向重复尝试。 +## 场景 7:推送通知 +用户输入示例: +```text +推送到达后通知扩展里下载图片偶尔失败,而且用户点击通知跳转的页面不对,帮我排查。 +``` + +通过标准: +- 识别 Notification Service Extension 的内存限制和 30 秒超时。 +- 指出 Extension 只有配置相同 Keychain Access Group 才能访问共享 Keychain item,且不应在 Extension 内发起长时网络请求。 +- 输出保持"根因 / 为什么 / 修法 / 验证"。 + +失败信号: +- 未考虑 Extension 的内存/时间/沙盒限制。 +- 跳过推送到达 → 用户点击 → 路由跳转的链路分析。 +- 建议在 AppDelegate 内硬编码通知路由映射。 + +## 场景 8:隐私权限 +用户输入示例: +```text +App 首次启动请求相机权限,用户拒绝后功能不可用,也没有引导去设置。另外 ATT 弹窗时机不对,审核被拒了。 +``` + +通过标准: +- 指出权限必须在用户明确行为上下文内请求,不可在启动时批量弹。 +- 给出权限被拒后的降级路径。 +- ATT 弹窗前需要 pre-permission 说明弹窗。 + +失败信号: +- 未检查 Info.plist 中 UsageDescription key。 +- 建议在 denied 状态下重试系统权限弹窗(无效操作)。 +- 未提及审核拒审风险。 + +## 场景 9:持久化与迁移 +用户输入示例: +```text +Core Data 加了新字段后迁移失败,数据丢了。现在想迁到 SwiftData,但不知道能不能平滑过渡。 +``` + +通过标准: +- 区分轻量级迁移和重量级迁移场景。 +- 迁移前必须备份,失败时不清空数据。 +- 指出 context 线程模型约束。 + +失败信号: +- 建议直接清空 persistent store 重建而不备份。 +- 建议跨 context 传递 NSManagedObject。 +- 在 SwiftData 方案中建议混用 NSFetchedResultsController。 + +## 场景 10:App Extensions +用户输入示例: +```text +Widget 刷新偶尔不更新数据,而且点击 Widget 跳转到主 App 时会闪退。Share Extension 也访问不到主 App 的用户 token。 +``` + +通过标准: +- 识别 Extension 与主 App 的数据共享必须通过 App Group / Keychain Group。 +- 指出 Widget getTimeline 内的内存和时间 budget。 +- 指出 Extension 独立进程的沙盒限制。 + +失败信号: +- 建议 Extension 直接访问主 App 沙盒目录或 UserDefaults.standard。 +- 建议在 getTimeline 内做重请求或复杂 image processing。 +- 忽略 Extension 独立进程、内存受限的事实。 + +## 场景 11:StoreKit / 内购 +用户输入示例: +```text +订阅购买后偶尔不到账,恢复购买也不稳定。现在客户端把到期时间存在 UserDefaults,服务端验证失败就切 sandbox 重试。 +``` + +通过标准: +- 指出购买状态不能只依赖 `UserDefaults`,必须以 StoreKit 交易或服务端验证结果为准。 +- 指出 `Transaction.updates` / `SKPaymentTransactionObserver` 需要在 App 生命周期内持续监听。 +- 指出 production 到 sandbox 的 fallback 只能在明确 sandbox receipt 指示时发生,不能吞掉所有生产验证失败。 + +失败信号: +- 建议用本地缓存直接判定订阅权益。 +- 每次启动都调用 `AppStore.sync()` / `restoreCompletedTransactions()`。 +- 把所有服务端验证失败都 fallback 到 sandbox。 + ## 记录模板 ```text 验证场景 diff --git a/skills-engineering/ios-engineer/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/scripts/append_usage_entry.sh index 6a0b8a7..ba78216 100755 --- a/skills-engineering/ios-engineer/scripts/append_usage_entry.sh +++ b/skills-engineering/ios-engineer/scripts/append_usage_entry.sh @@ -13,7 +13,7 @@ usage() { cat <<'USAGE' Usage: bash scripts/append_usage_entry.sh \ --tool \ - --task-type \ + --task-type \ --prompt-summary "<5-200 char Chinese summary>" \ --expected-rules "ID1,ID2,..." \ --hit-rules "ID1,..." \ @@ -81,7 +81,7 @@ tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze +ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control notifications privacy persistence storekit extensions other].freeze ALLOWED_OUTCOMES = %w[pass partial fail].freeze ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze ID_FORMAT = /\A[A-Z]+-\d{3}\z/ diff --git a/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh new file mode 100755 index 0000000..fc535e9 --- /dev/null +++ b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +HISTORY_DIR="evolution/history" +ACTIVE_VERSION_FILE="evolution/active_version.json" +KEEP_RECENT="${KEEP_RECENT:-10}" +MILESTONE_INTERVAL="${MILESTONE_INTERVAL:-10}" + +DRY_RUN=false +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + -h|--help) + echo "Usage: bash scripts/gc_evolution_history.sh [--dry-run]" + echo "" + echo "Clean up old evolution history snapshots, keeping:" + echo " - Most recent ${KEEP_RECENT} versions" + echo " - Every ${MILESTONE_INTERVAL}th version as milestones (v10, v20, ...)" + echo " - Current active version (always protected)" + echo "" + echo "Options:" + echo " --dry-run List what would be deleted without actually deleting" + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +if [ ! -d "$HISTORY_DIR" ]; then + echo "No history directory found: ${HISTORY_DIR}" + exit 0 +fi + +# Get active version +if [ -f "$ACTIVE_VERSION_FILE" ]; then + ACTIVE_VERSION="$(ruby -rjson -e 'puts JSON.parse(File.read(ARGV[0]))["active_version"]' "$ACTIVE_VERSION_FILE")" +else + echo "No active_version.json found, aborting GC" + exit 0 +fi + +# Collect all version dirs, extract version number for sorting +tmpfile="$(mktemp)" +trap 'rm -f "$tmpfile"' EXIT + +for dir in "$HISTORY_DIR"/v[0-9]*/; do + [ -d "$dir" ] || continue + dirname="$(basename "$dir")" + # Match versions created by promote_skill_evolution.sh: v1, v10, v73, v73-alpha, v73-hotfix + if ! echo "$dirname" | grep -qE '^v[0-9]+(-[A-Za-z0-9]+)*$'; then + continue + fi + # Extract leading numeric portion for milestone calculation (v73-alpha → 73) + num="$(echo "$dirname" | sed 's/^v//; s/-.*//' | sed 's/^0*//')" + num="${num:-0}" + echo "$num $dirname" >> "$tmpfile" +done + +if [ ! -s "$tmpfile" ]; then + echo "No versioned history directories found" + exit 0 +fi + +# Sort by version number descending +sorted_dirs=($(sort -k1 -n -r "$tmpfile" | awk '{print $2}')) +total="${#sorted_dirs[@]}" + +# Mark protected versions using a temp file +protected_file="$(mktemp)" +trap 'rm -f "$tmpfile" "$protected_file"' EXIT + +# Active version is always protected +echo "$ACTIVE_VERSION" >> "$protected_file" + +# Most recent KEEP_RECENT +count=0 +for v in "${sorted_dirs[@]}"; do + if [ "$count" -lt "$KEEP_RECENT" ]; then + echo "$v" >> "$protected_file" + fi + count=$((count + 1)) +done + +# Milestones (v10, v20, ...) — use only the leading numeric portion for suffixed versions +for v in "${sorted_dirs[@]}"; do + num="$(echo "$v" | sed 's/^v//; s/-.*//' | sed 's/^0*//')" + num="${num:-0}" + if [ "$num" -ge "$MILESTONE_INTERVAL" ] && [ $((num % MILESTONE_INTERVAL)) -eq 0 ]; then + echo "$v" >> "$protected_file" + fi +done + +echo "=== Evolution History GC ===" +echo "Active version: $ACTIVE_VERSION" +echo "Keep recent: $KEEP_RECENT" +echo "Total versions found: $total" +echo "" + +deleted=0 +kept=0 +for v in "${sorted_dirs[@]}"; do + if grep -qx "$v" "$protected_file"; then + echo " [KEEP] $HISTORY_DIR/$v" + kept=$((kept + 1)) + else + echo " [DELETE] $HISTORY_DIR/$v" + if ! $DRY_RUN; then + rm -rf "$HISTORY_DIR/$v" + fi + deleted=$((deleted + 1)) + fi +done + +echo "" +if $DRY_RUN; then + echo "DRY RUN: Would delete $deleted version(s), keep $kept version(s)" +else + echo "Done: Deleted $deleted version(s), kept $kept version(s)" +fi diff --git a/skills-engineering/ios-engineer/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/scripts/promote_skill_evolution.sh index e9f174d..a5f8c39 100755 --- a/skills-engineering/ios-engineer/scripts/promote_skill_evolution.sh +++ b/skills-engineering/ios-engineer/scripts/promote_skill_evolution.sh @@ -105,4 +105,16 @@ if [ -n "$proposal_file" ]; then bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null fi +if [ "${SKIP_EVOLUTION_GC:-0}" != "1" ]; then + # Dry-run first to surface deletion count before committing + gc_dry="$(bash scripts/gc_evolution_history.sh --dry-run 2>/dev/null)" || true + delete_count="$(echo "$gc_dry" | grep -c '\[DELETE\]' || true)" + if [ "${delete_count:-0}" -gt 0 ]; then + echo "Note: GC will remove ${delete_count} old history snapshot(s). Set SKIP_EVOLUTION_GC=1 before this script to skip GC." >&2 + fi + if ! bash scripts/gc_evolution_history.sh; then + echo "Warning: evolution history GC failed; promotion already completed. Run scripts/gc_evolution_history.sh manually." >&2 + fi +fi + echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/scripts/validate.sh b/skills-engineering/ios-engineer/scripts/validate.sh new file mode 100755 index 0000000..fe326d6 --- /dev/null +++ b/skills-engineering/ios-engineer/scripts/validate.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +usage() { + cat <<'USAGE' +Usage: bash scripts/validate.sh [OPTIONS] + +Unified validation entry for ios-engineer skill evolution pipeline. + +Options: + --all Run all validation steps (default) + --quick Run fast checks only: YAML, line count, ref existence, rule IDs + --scenarios Validate scenario specs exclusively + --links Validate internal markdown links exclusively + --ledger Validate usage ledger exclusively + --ids Validate rule IDs exclusively + --skip-snapshot Skip snapshot consistency check + --skip-behavior Skip behavior validation scenarios + +Exit 0: all validations pass +Exit 1: any validation step fails +USAGE + exit 1 +} + +ALL=true +QUICK=false +SCENARIOS=false +LINKS=false +LEDGER=false +IDS=false +SKIP_SNAPSHOT="${SKIP_SNAPSHOT_CONSISTENCY:-0}" +SKIP_BEHAVIOR="${SKIP_BEHAVIOR_VALIDATION:-0}" + +while [ $# -gt 0 ]; do + case "$1" in + --all) ALL=true; shift ;; + --quick) QUICK=true; ALL=false; shift ;; + --scenarios) SCENARIOS=true; ALL=false; shift ;; + --links) LINKS=true; ALL=false; shift ;; + --ledger) LEDGER=true; ALL=false; shift ;; + --ids) IDS=true; ALL=false; shift ;; + --skip-snapshot) SKIP_SNAPSHOT=1; shift ;; + --skip-behavior) SKIP_BEHAVIOR=1; shift ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +run_step() { + local num="$1"; shift + local desc="$1"; shift + echo "[${num}] ${desc}" + "$@" +} + +failures=0 + +if $QUICK; then + echo "=== Quick Validation ===" + + run_step "1/4" "Validate YAML structure" bash -c \ + 'ruby -e "require \"yaml\"; YAML.load_file(\"SKILL.md\"); YAML.load_file(\"agents/openai.yaml\"); puts \"YAML OK\""' + + run_step "2/4" "Validate SKILL.md line count" bash -c \ + 'lines=$(wc -l < SKILL.md | tr -d " "); [ "$lines" -le 500 ] && echo "$lines lines OK" || { echo "FAIL: $lines > 500"; exit 1; }' + + run_step "3/4" "Validate ref existence" bash scripts/validate_scenario_specs.sh <<<'skip' + + # Quick ref check: all references/*.md mentioned in SKILL.md exist + missing=0 + while IFS= read -r path; do + [ -z "$path" ] && continue + if [ ! -f "$path" ]; then + echo "Missing reference: $path" + missing=1 + fi + done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) + if [ "$missing" -ne 0 ]; then + echo "FAIL: missing references" + ((failures++)) || true + else + echo "Reference files OK" + fi + + run_step "4/4" "Validate rule IDs" bash scripts/validate_rule_ids.sh +fi + +if $SCENARIOS; then + echo "=== Scenario Validation ===" + run_step "S1" "Validate scenario specs" bash scripts/validate_scenario_specs.sh + run_step "S2" "Validate internal markdown links" bash -c \ + 'ruby <<'"'"'RUBY'"'"' +broken = 0 +Dir.glob("references/*.md").sort.each do |file| + File.foreach(file).with_index(1) do |line, lineno| + line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| + next if link =~ /\A(https?|mailto):/i + path = link.split("#", 2).first.to_s + next if path.empty? + full = File.expand_path(path, File.dirname(file)) + unless File.exist?(full) + puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" + broken += 1 + end + end + end +end +exit 1 if broken > 0 +puts "Internal links OK" +RUBY' +fi + +if $LINKS; then + echo "=== Link Validation ===" + run_step "L1" "Validate internal markdown links" bash -c \ + 'ruby <<'"'"'RUBY'"'"' +broken = 0 +Dir.glob("references/*.md").sort.each do |file| + File.foreach(file).with_index(1) do |line, lineno| + line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| + next if link =~ /\A(https?|mailto):/i + path = link.split("#", 2).first.to_s + next if path.empty? + full = File.expand_path(path, File.dirname(file)) + unless File.exist?(full) + puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" + broken += 1 + end + end + end +end +exit 1 if broken > 0 +puts "Internal links OK" +RUBY' +fi + +if $LEDGER; then + echo "=== Ledger Validation ===" + run_step "LD1" "Validate usage ledger" bash scripts/validate_usage_ledger.sh +fi + +if $IDS; then + echo "=== Rule ID Validation ===" + run_step "ID1" "Validate rule IDs" bash scripts/validate_rule_ids.sh +fi + +if $ALL; then + echo "=== Full Validation Pipeline ===" + SKIP_SNAPSHOT_CONSISTENCY="$SKIP_SNAPSHOT" \ + SKIP_BEHAVIOR_VALIDATION="$SKIP_BEHAVIOR" \ + bash scripts/validate_skill_evolution.sh + exit $? +fi + +if [ "$failures" -gt 0 ]; then + echo "FAILED: ${failures} step(s) failed" + exit 1 +fi + +echo "All checks passed" diff --git a/skills-engineering/ios-engineer/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/scripts/validate_scenario_specs.sh index 696f825..2af91c6 100755 --- a/skills-engineering/ios-engineer/scripts/validate_scenario_specs.sh +++ b/skills-engineering/ios-engineer/scripts/validate_scenario_specs.sh @@ -26,6 +26,11 @@ CANONICAL_SLUGS = %w[ review migration mcp-control + notifications + privacy + persistence + storekit + extensions ].freeze OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze diff --git a/skills-engineering/ios-engineer/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/scripts/validate_skill_evolution.sh index 6ed9739..3cd529c 100755 --- a/skills-engineering/ios-engineer/scripts/validate_skill_evolution.sh +++ b/skills-engineering/ios-engineer/scripts/validate_skill_evolution.sh @@ -5,10 +5,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT_DIR" -echo "[1/13] Validate YAML structure" +echo "[1/14] Validate YAML structure" ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' -echo "[2/13] Validate SKILL.md size" +echo "[2/14] Validate SKILL.md size" line_count="$(wc -l < SKILL.md | tr -d ' ')" if [ "$line_count" -gt 500 ]; then echo "SKILL.md too long: ${line_count} lines" @@ -16,7 +16,7 @@ if [ "$line_count" -gt 500 ]; then fi echo "SKILL.md lines: ${line_count}" -echo "[3/13] Validate referenced files exist" +echo "[3/14] Validate referenced files exist" missing=0 while IFS= read -r path; do [ -z "$path" ] && continue @@ -31,7 +31,7 @@ if [ "$missing" -ne 0 ]; then fi echo "Reference files OK" -echo "[4/13] Validate layering guardrails" +echo "[4/14] Validate layering guardrails" if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then echo "root_cause_enforcement.md should not define MCP control sections" exit 1 @@ -44,7 +44,7 @@ fi echo "Layering guardrails OK" -echo "[5/13] Validate internal markdown links" +echo "[5/14] Validate internal markdown links" ruby <<'RUBY' broken = 0 Dir.glob('references/*.md').sort.each do |file| @@ -65,16 +65,16 @@ exit 1 if broken > 0 RUBY echo "Internal links OK" -echo "[6/13] Validate scenario specs" +echo "[6/14] Validate scenario specs" bash scripts/validate_scenario_specs.sh -echo "[7/13] Validate rule IDs" +echo "[7/14] Validate rule IDs" bash scripts/validate_rule_ids.sh -echo "[8/13] Validate usage ledger" +echo "[8/14] Validate usage ledger" bash scripts/validate_usage_ledger.sh -echo "[9/13] Validate no orphan references" +echo "[9/14] Validate no orphan references" ruby <<'RUBY' referenced = {} # SKILL.md 直接引用 @@ -102,7 +102,7 @@ end RUBY echo "No orphan references" -echo "[10/13] Validate unique ownership + retired word regression" +echo "[10/14] Validate unique ownership + retired word regression" ruby <<'RUBY' # pattern => [expected_owner_basename, description] UNIQUE_OWNERS = { @@ -145,7 +145,7 @@ exit 1 if violations > 0 RUBY echo "Unique ownership + retired words OK" -echo "[11/13] Validate threshold doc/script sync" +echo "[11/14] Validate threshold doc/script sync" ruby <<'RUBY' script_path = "scripts/summarize_usage_ledger.sh" doc_path = "references/usage_ledger.md" @@ -191,18 +191,90 @@ end RUBY echo "Threshold doc/script sync OK" -echo "[12/13] Validate snapshot consistency with active version" +echo "[12/14] Validate snapshot consistency with active version" if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" else bash scripts/check_snapshot_consistency.sh fi -echo "[13/13] Run behavior validation scenarios" +echo "[13/14] Run behavior validation scenarios" if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" else SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh fi +echo "[14/14] Validate slug list sync (validation_scenarios.md ↔ ALLOWED_TASK_TYPES ↔ CANONICAL_SLUGS)" +ruby <<'RUBY' +scenarios_path = "references/validation_scenarios.md" +ledger_validator = "scripts/validate_usage_ledger.sh" +spec_validator = "scripts/validate_scenario_specs.sh" + +# 1. Extract slugs from validation_scenarios.md +# Looks for the line: 建议使用固定场景标识:`slug1`、`slug2`、... +scenario_slugs = [] +File.foreach(scenarios_path) do |line| + if line.include?("建议使用固定场景标识") + scenario_slugs = line.scan(/`([a-z][a-z0-9-]*)`/).flatten + break + end +end +if scenario_slugs.empty? + puts "FAIL: could not parse slug list from #{scenarios_path}" + exit 1 +end + +# 2. Extract ALLOWED_TASK_TYPES from validate_usage_ledger.sh (exclude 'other') +ledger_types = [] +File.foreach(ledger_validator) do |line| + m = line.match(/ALLOWED_TASK_TYPES\s*=\s*%w\[([^\]]+)\]/) + if m + ledger_types = m[1].split.reject { |s| s == "other" } + break + end +end +if ledger_types.empty? + puts "FAIL: could not parse ALLOWED_TASK_TYPES from #{ledger_validator}" + exit 1 +end + +# 3. Extract CANONICAL_SLUGS from validate_scenario_specs.sh +canonical_slugs = [] +in_block = false +File.foreach(spec_validator) do |line| + in_block = true if line =~ /CANONICAL_SLUGS\s*=\s*%w\[/ + if in_block + break if line.include?("].freeze") + canonical_slugs += line.scan(/\b([a-z][a-z0-9-]+)\b/).flatten + end +end +if canonical_slugs.empty? + puts "FAIL: could not parse CANONICAL_SLUGS from #{spec_validator}" + exit 1 +end + +errors = [] + +# scenario_slugs ↔ ledger_types +missing = scenario_slugs - ledger_types +extra = ledger_types - scenario_slugs +errors << "In validation_scenarios.md but not ALLOWED_TASK_TYPES: #{missing.join(', ')}" unless missing.empty? +errors << "In ALLOWED_TASK_TYPES but not validation_scenarios.md: #{extra.join(', ')}" unless extra.empty? + +# scenario_slugs ↔ canonical_slugs +missing2 = scenario_slugs - canonical_slugs +extra2 = canonical_slugs - scenario_slugs +errors << "In validation_scenarios.md but not CANONICAL_SLUGS: #{missing2.join(', ')}" unless missing2.empty? +errors << "In CANONICAL_SLUGS but not validation_scenarios.md: #{extra2.join(', ')}" unless extra2.empty? + +if errors.empty? + puts "Slug sync OK (#{scenario_slugs.length} slugs: #{scenario_slugs.join(', ')})" +else + errors.each { |e| puts e } + exit 1 +end +RUBY +echo "Slug sync OK" + echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/scripts/validate_usage_ledger.sh index 72591a8..097d293 100755 --- a/skills-engineering/ios-engineer/scripts/validate_usage_ledger.sh +++ b/skills-engineering/ios-engineer/scripts/validate_usage_ledger.sh @@ -26,7 +26,7 @@ ledger_path = "evolution/usage/usage.jsonl" index_path = "references/rule_index.md" ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze +ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control notifications privacy persistence storekit extensions other].to_set.freeze ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze ID_FORMAT = /\A[A-Z]+-\d{3}\z/ From 38e740a86f3b55e870adbadfdfdfad2455f83684 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 17:11:11 +0800 Subject: [PATCH 17/30] =?UTF-8?q?feat(ios-engineer):=20gc=5Fevolution=5Fhi?= =?UTF-8?q?story.sh=20=E5=A2=9E=E5=8A=A0=E5=88=A0=E9=99=A4=E5=89=8D?= =?UTF-8?q?=E9=A2=84=E6=95=B0=E6=8F=90=E7=A4=BA=E5=B9=B6=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=BF=AB=E7=85=A7=20GC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 非 dry-run 模式下,删除前先预数删除量,将"将删除 N 个快照"打印到 stderr - 对 evolution/history/ 执行 GC:删除 61 个旧快照,保留 16 个(v64-v73 最近 10 个 + v10/v20/v30/v40/v50/v60 里程碑) --- .../evolution/history/v1/metadata.json | 5 - .../evolution/history/v1/snapshot/SKILL.md | 92 ----- .../history/v1/snapshot/agents/openai.yaml | 4 - .../v1/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 105 ------ .../references/build_release_and_ci.md | 88 ----- .../v1/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 87 ----- .../v1/snapshot/references/domain_modeling.md | 94 ----- .../v1/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 112 ------ .../v1/snapshot/references/layout_and_ui.md | 84 ----- .../v1/snapshot/references/mcp_control.md | 50 --- .../references/migration_risk_control.md | 64 ---- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../references/refactoring_and_migration.md | 66 ---- .../snapshot/references/review_checklists.md | 88 ----- .../references/root_cause_enforcement.md | 109 ------ .../v1/snapshot/references/self_evolution.md | 112 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../v1/snapshot/references/terminology.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 128 ------- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/promote_skill_evolution.sh | 50 --- .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/validate_skill_evolution.sh | 46 --- .../evolution/history/v11/metadata.json | 5 - .../evolution/history/v11/snapshot/SKILL.md | 48 --- .../history/v11/snapshot/agents/openai.yaml | 4 - .../v11/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v11/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 96 ----- .../v11/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v11/snapshot/references/layout_and_ui.md | 156 -------- .../v11/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v11/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v11/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v11/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v12/metadata.json | 5 - .../evolution/history/v12/snapshot/SKILL.md | 47 --- .../history/v12/snapshot/agents/openai.yaml | 4 - .../v12/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v12/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 96 ----- .../v12/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v12/snapshot/references/layout_and_ui.md | 156 -------- .../v12/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v12/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v12/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v12/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v13/metadata.json | 5 - .../evolution/history/v13/snapshot/SKILL.md | 47 --- .../history/v13/snapshot/agents/openai.yaml | 4 - .../v13/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v13/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 96 ----- .../v13/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v13/snapshot/references/layout_and_ui.md | 156 -------- .../v13/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v13/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v13/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v13/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v14/metadata.json | 5 - .../evolution/history/v14/snapshot/SKILL.md | 47 --- .../history/v14/snapshot/agents/openai.yaml | 4 - .../v14/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v14/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 96 ----- .../v14/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v14/snapshot/references/layout_and_ui.md | 156 -------- .../v14/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v14/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v14/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v14/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v15/metadata.json | 5 - .../evolution/history/v15/snapshot/SKILL.md | 47 --- .../history/v15/snapshot/agents/openai.yaml | 4 - .../v15/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v15/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 96 ----- .../v15/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v15/snapshot/references/layout_and_ui.md | 156 -------- .../v15/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v15/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v15/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v15/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v16/metadata.json | 5 - .../evolution/history/v16/snapshot/SKILL.md | 47 --- .../history/v16/snapshot/agents/openai.yaml | 4 - .../v16/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v16/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v16/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v16/snapshot/references/layout_and_ui.md | 156 -------- .../v16/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v16/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v16/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v16/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v17/metadata.json | 5 - .../evolution/history/v17/snapshot/SKILL.md | 47 --- .../history/v17/snapshot/agents/openai.yaml | 4 - .../v17/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v17/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v17/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v17/snapshot/references/layout_and_ui.md | 156 -------- .../v17/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v17/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v17/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v17/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v18/metadata.json | 5 - .../evolution/history/v18/snapshot/SKILL.md | 47 --- .../history/v18/snapshot/agents/openai.yaml | 4 - .../v18/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v18/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v18/snapshot/references/examples.md | 163 -------- .../references/execution_playbooks.md | 114 ------ .../v18/snapshot/references/layout_and_ui.md | 156 -------- .../v18/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v18/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v18/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v18/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v19/metadata.json | 5 - .../evolution/history/v19/snapshot/SKILL.md | 47 --- .../history/v19/snapshot/agents/openai.yaml | 4 - .../v19/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v19/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v19/snapshot/references/examples.md | 168 --------- .../references/execution_playbooks.md | 114 ------ .../v19/snapshot/references/layout_and_ui.md | 156 -------- .../v19/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v19/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v19/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v19/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v2-drill/metadata.json | 5 - .../history/v2-drill/snapshot/SKILL.md | 92 ----- .../v2-drill/snapshot/agents/openai.yaml | 4 - .../snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 105 ------ .../references/build_release_and_ci.md | 88 ----- .../snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 87 ----- .../snapshot/references/domain_modeling.md | 94 ----- .../v2-drill/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 112 ------ .../snapshot/references/layout_and_ui.md | 84 ----- .../snapshot/references/mcp_control.md | 50 --- .../references/migration_risk_control.md | 64 ---- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../references/refactoring_and_migration.md | 66 ---- .../snapshot/references/review_checklists.md | 88 ----- .../references/root_cause_enforcement.md | 109 ------ .../snapshot/references/self_evolution.md | 112 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/terminology.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 128 ------- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/promote_skill_evolution.sh | 50 --- .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/validate_skill_evolution.sh | 46 --- .../history/v2-status-flow/metadata.json | 5 - .../history/v2-status-flow/snapshot/SKILL.md | 92 ----- .../snapshot/agents/openai.yaml | 4 - .../snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 105 ------ .../references/build_release_and_ci.md | 88 ----- .../snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 87 ----- .../snapshot/references/domain_modeling.md | 94 ----- .../snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 112 ------ .../snapshot/references/layout_and_ui.md | 84 ----- .../snapshot/references/mcp_control.md | 50 --- .../references/migration_risk_control.md | 64 ---- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../references/refactoring_and_migration.md | 66 ---- .../snapshot/references/review_checklists.md | 88 ----- .../references/root_cause_enforcement.md | 109 ------ .../snapshot/references/self_evolution.md | 114 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/terminology.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 128 ------- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/promote_skill_evolution.sh | 55 --- .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 44 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 56 --- .../evolution/history/v2/metadata.json | 5 - .../evolution/history/v2/snapshot/SKILL.md | 116 ------ .../history/v2/snapshot/agents/openai.yaml | 4 - .../v2/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 107 ------ .../references/build_release_and_ci.md | 88 ----- .../v2/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v2/snapshot/references/domain_modeling.md | 96 ----- .../v2/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v2/snapshot/references/layout_and_ui.md | 156 -------- .../v2/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v2/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../v2/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v21/metadata.json | 5 - .../evolution/history/v21/snapshot/SKILL.md | 47 --- .../history/v21/snapshot/agents/openai.yaml | 4 - .../v21/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v21/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v21/snapshot/references/examples.md | 168 --------- .../references/execution_playbooks.md | 114 ------ .../v21/snapshot/references/layout_and_ui.md | 156 -------- .../v21/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v21/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v21/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v21/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v22/metadata.json | 5 - .../evolution/history/v22/snapshot/SKILL.md | 47 --- .../history/v22/snapshot/agents/openai.yaml | 4 - .../v22/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v22/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v22/snapshot/references/examples.md | 168 --------- .../references/execution_playbooks.md | 114 ------ .../v22/snapshot/references/layout_and_ui.md | 156 -------- .../v22/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v22/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v22/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v22/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v23/metadata.json | 5 - .../evolution/history/v23/snapshot/SKILL.md | 47 --- .../history/v23/snapshot/agents/openai.yaml | 4 - .../v23/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v23/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v23/snapshot/references/examples.md | 168 --------- .../references/execution_playbooks.md | 114 ------ .../v23/snapshot/references/layout_and_ui.md | 156 -------- .../v23/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 109 ------ .../v23/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v23/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v23/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v24/metadata.json | 5 - .../evolution/history/v24/snapshot/SKILL.md | 48 --- .../history/v24/snapshot/agents/openai.yaml | 4 - .../v24/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 96 ----- .../v24/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v24/snapshot/references/examples.md | 168 --------- .../references/execution_playbooks.md | 114 ------ .../v24/snapshot/references/layout_and_ui.md | 156 -------- .../v24/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 115 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v24/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v24/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v24/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v25/metadata.json | 5 - .../evolution/history/v25/snapshot/SKILL.md | 48 --- .../history/v25/snapshot/agents/openai.yaml | 4 - .../v25/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 129 ------- .../references/build_release_and_ci.md | 96 ----- .../v25/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v25/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../v25/snapshot/references/layout_and_ui.md | 156 -------- .../v25/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v25/snapshot/references/self_evolution.md | 123 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v25/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v25/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v26/metadata.json | 5 - .../evolution/history/v26/snapshot/SKILL.md | 48 --- .../history/v26/snapshot/agents/openai.yaml | 4 - .../v26/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 129 ------- .../references/build_release_and_ci.md | 96 ----- .../v26/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v26/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../v26/snapshot/references/layout_and_ui.md | 156 -------- .../v26/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v26/snapshot/references/self_evolution.md | 125 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v26/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v26/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v27/metadata.json | 5 - .../evolution/history/v27/snapshot/SKILL.md | 48 --- .../history/v27/snapshot/agents/openai.yaml | 4 - .../v27/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v27/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v27/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../v27/snapshot/references/layout_and_ui.md | 156 -------- .../v27/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v27/snapshot/references/self_evolution.md | 125 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v27/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v27/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v28/metadata.json | 5 - .../evolution/history/v28/snapshot/SKILL.md | 48 --- .../history/v28/snapshot/agents/openai.yaml | 4 - .../v28/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v28/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v28/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../v28/snapshot/references/layout_and_ui.md | 156 -------- .../v28/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v28/snapshot/references/self_evolution.md | 125 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v28/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v28/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v29/metadata.json | 5 - .../evolution/history/v29/snapshot/SKILL.md | 48 --- .../history/v29/snapshot/agents/openai.yaml | 4 - .../v29/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v29/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v29/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../v29/snapshot/references/layout_and_ui.md | 156 -------- .../v29/snapshot/references/mcp_control.md | 46 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v29/snapshot/references/self_evolution.md | 125 ------ .../snapshot/references/swift_concurrency.md | 62 --- .../v29/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v29/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v3/metadata.json | 5 - .../evolution/history/v3/snapshot/SKILL.md | 98 ----- .../history/v3/snapshot/agents/openai.yaml | 4 - .../v3/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 107 ------ .../references/build_release_and_ci.md | 88 ----- .../v3/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v3/snapshot/references/domain_modeling.md | 96 ----- .../v3/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v3/snapshot/references/layout_and_ui.md | 156 -------- .../v3/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v3/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../v3/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v31/metadata.json | 5 - .../evolution/history/v31/snapshot/SKILL.md | 60 --- .../history/v31/snapshot/agents/openai.yaml | 4 - .../v31/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v31/snapshot/references/code_templates.md | 272 ------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v31/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v31/snapshot/references/layout_and_ui.md | 156 -------- .../v31/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v31/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 137 ------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v32/metadata.json | 5 - .../evolution/history/v32/snapshot/SKILL.md | 60 --- .../history/v32/snapshot/agents/openai.yaml | 4 - .../v32/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v32/snapshot/references/code_templates.md | 275 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v32/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v32/snapshot/references/layout_and_ui.md | 156 -------- .../v32/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v32/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 137 ------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v33/metadata.json | 5 - .../evolution/history/v33/snapshot/SKILL.md | 60 --- .../history/v33/snapshot/agents/openai.yaml | 4 - .../v33/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v33/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v33/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v33/snapshot/references/layout_and_ui.md | 156 -------- .../v33/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v33/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../snapshot/scripts/test_proposal_scripts.sh | 95 ----- .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 144 ------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v34/metadata.json | 5 - .../evolution/history/v34/snapshot/SKILL.md | 60 --- .../history/v34/snapshot/agents/openai.yaml | 4 - .../v34/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v34/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v34/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v34/snapshot/references/layout_and_ui.md | 156 -------- .../v34/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v34/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 82 ---- .../snapshot/scripts/test_proposal_scripts.sh | 95 ----- .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v35/metadata.json | 5 - .../evolution/history/v35/snapshot/SKILL.md | 60 --- .../history/v35/snapshot/agents/openai.yaml | 4 - .../v35/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v35/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v35/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v35/snapshot/references/layout_and_ui.md | 156 -------- .../v35/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v35/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 95 ----- .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v36/metadata.json | 5 - .../evolution/history/v36/snapshot/SKILL.md | 62 --- .../history/v36/snapshot/agents/openai.yaml | 4 - .../v36/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v36/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v36/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v36/snapshot/references/layout_and_ui.md | 156 -------- .../v36/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v36/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 96 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 95 ----- .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v37/metadata.json | 5 - .../evolution/history/v37/snapshot/SKILL.md | 62 --- .../history/v37/snapshot/agents/openai.yaml | 4 - .../v37/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v37/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v37/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v37/snapshot/references/layout_and_ui.md | 156 -------- .../v37/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 109 ------ .../v37/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 96 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v38/metadata.json | 5 - .../evolution/history/v38/snapshot/SKILL.md | 62 --- .../history/v38/snapshot/agents/openai.yaml | 4 - .../v38/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v38/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v38/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v38/snapshot/references/layout_and_ui.md | 156 -------- .../v38/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v38/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v39/metadata.json | 5 - .../evolution/history/v39/snapshot/SKILL.md | 62 --- .../history/v39/snapshot/agents/openai.yaml | 4 - .../v39/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v39/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v39/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 114 ------ .../snapshot/references/ios_conventions.md | 130 ------- .../v39/snapshot/references/layout_and_ui.md | 156 -------- .../v39/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v39/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 151 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../history/v4-approved-drill/metadata.json | 5 - .../v4-approved-drill/snapshot/SKILL.md | 92 ----- .../snapshot/agents/openai.yaml | 4 - .../snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 105 ------ .../references/build_release_and_ci.md | 88 ----- .../snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 87 ----- .../snapshot/references/domain_modeling.md | 94 ----- .../snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 112 ------ .../snapshot/references/layout_and_ui.md | 84 ----- .../snapshot/references/mcp_control.md | 50 --- .../references/migration_risk_control.md | 64 ---- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../references/refactoring_and_migration.md | 66 ---- .../snapshot/references/review_checklists.md | 88 ----- .../references/root_cause_enforcement.md | 109 ------ .../snapshot/references/self_evolution.md | 121 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/terminology.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/promote_skill_evolution.sh | 87 ----- .../scripts/record_validation_scenario.sh | 91 ----- .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 44 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v4/metadata.json | 5 - .../evolution/history/v4/snapshot/SKILL.md | 90 ----- .../history/v4/snapshot/agents/openai.yaml | 4 - .../v4/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 107 ------ .../references/build_release_and_ci.md | 88 ----- .../v4/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v4/snapshot/references/domain_modeling.md | 96 ----- .../v4/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v4/snapshot/references/layout_and_ui.md | 156 -------- .../v4/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v4/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v4/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v4/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v41/metadata.json | 5 - .../evolution/history/v41/snapshot/SKILL.md | 63 ---- .../history/v41/snapshot/agents/openai.yaml | 4 - .../v41/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v41/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v41/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v41/snapshot/references/layout_and_ui.md | 156 -------- .../v41/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v41/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 153 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v42/metadata.json | 5 - .../evolution/history/v42/snapshot/SKILL.md | 63 ---- .../history/v42/snapshot/agents/openai.yaml | 4 - .../v42/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v42/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v42/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v42/snapshot/references/layout_and_ui.md | 156 -------- .../v42/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v42/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_skill_evolution.sh | 153 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v43/metadata.json | 5 - .../evolution/history/v43/snapshot/SKILL.md | 63 ---- .../history/v43/snapshot/agents/openai.yaml | 4 - .../v43/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v43/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v43/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v43/snapshot/references/layout_and_ui.md | 156 -------- .../v43/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v43/snapshot/references/self_evolution.md | 127 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 150 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../scripts/validate_scenario_specs.sh | 188 --------- .../scripts/validate_skill_evolution.sh | 156 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v44/metadata.json | 5 - .../evolution/history/v44/snapshot/SKILL.md | 63 ---- .../history/v44/snapshot/agents/openai.yaml | 4 - .../v44/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v44/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v44/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v44/snapshot/references/layout_and_ui.md | 156 -------- .../v44/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v44/snapshot/references/rule_index.md | 80 ---- .../v44/snapshot/references/self_evolution.md | 136 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 153 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v44/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 159 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v45/metadata.json | 5 - .../evolution/history/v45/snapshot/SKILL.md | 63 ---- .../history/v45/snapshot/agents/openai.yaml | 4 - .../v45/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v45/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v45/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v45/snapshot/references/layout_and_ui.md | 156 -------- .../v45/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v45/snapshot/references/rule_index.md | 80 ---- .../v45/snapshot/references/self_evolution.md | 136 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../references/validation_scenarios.md | 153 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v45/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 159 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../evolution/history/v46/metadata.json | 5 - .../evolution/history/v46/snapshot/SKILL.md | 63 ---- .../history/v46/snapshot/agents/openai.yaml | 4 - .../v46/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v46/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v46/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v46/snapshot/references/layout_and_ui.md | 156 -------- .../v46/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v46/snapshot/references/rule_index.md | 80 ---- .../v46/snapshot/references/self_evolution.md | 144 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v46/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v46/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v47/metadata.json | 5 - .../evolution/history/v47/snapshot/SKILL.md | 63 ---- .../history/v47/snapshot/agents/openai.yaml | 4 - .../v47/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v47/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v47/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v47/snapshot/references/layout_and_ui.md | 156 -------- .../v47/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v47/snapshot/references/rule_index.md | 80 ---- .../v47/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v47/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v47/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v48/metadata.json | 5 - .../evolution/history/v48/snapshot/SKILL.md | 62 --- .../history/v48/snapshot/agents/openai.yaml | 4 - .../v48/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v48/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v48/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v48/snapshot/references/layout_and_ui.md | 156 -------- .../v48/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v48/snapshot/references/rule_index.md | 79 ---- .../v48/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v48/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v48/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v49/metadata.json | 5 - .../evolution/history/v49/snapshot/SKILL.md | 61 --- .../history/v49/snapshot/agents/openai.yaml | 4 - .../v49/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v49/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v49/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v49/snapshot/references/layout_and_ui.md | 156 -------- .../v49/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v49/snapshot/references/rule_index.md | 79 ---- .../v49/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v49/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v49/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../history/v5-demo-flow/metadata.json | 5 - .../history/v5-demo-flow/snapshot/SKILL.md | 92 ----- .../v5-demo-flow/snapshot/agents/openai.yaml | 4 - .../snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 105 ------ .../references/build_release_and_ci.md | 88 ----- .../snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 87 ----- .../snapshot/references/domain_modeling.md | 94 ----- .../snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 112 ------ .../snapshot/references/layout_and_ui.md | 84 ----- .../snapshot/references/mcp_control.md | 50 --- .../references/migration_risk_control.md | 64 ---- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../references/refactoring_and_migration.md | 66 ---- .../snapshot/references/review_checklists.md | 88 ----- .../references/root_cause_enforcement.md | 109 ------ .../snapshot/references/self_evolution.md | 121 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../snapshot/references/team_collaboration.md | 55 --- .../snapshot/references/terminology.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 109 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 44 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v5/metadata.json | 5 - .../evolution/history/v5/snapshot/SKILL.md | 77 ---- .../history/v5/snapshot/agents/openai.yaml | 4 - .../v5/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 107 ------ .../references/build_release_and_ci.md | 88 ----- .../v5/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v5/snapshot/references/domain_modeling.md | 96 ----- .../v5/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v5/snapshot/references/layout_and_ui.md | 156 -------- .../v5/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v5/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v5/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v5/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v51/metadata.json | 5 - .../evolution/history/v51/snapshot/SKILL.md | 61 --- .../history/v51/snapshot/agents/openai.yaml | 4 - .../v51/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v51/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v51/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v51/snapshot/references/layout_and_ui.md | 156 -------- .../v51/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v51/snapshot/references/rule_index.md | 79 ---- .../v51/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v51/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v51/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v52/metadata.json | 5 - .../evolution/history/v52/snapshot/SKILL.md | 61 --- .../history/v52/snapshot/agents/openai.yaml | 4 - .../v52/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v52/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v52/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v52/snapshot/references/layout_and_ui.md | 156 -------- .../v52/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v52/snapshot/references/rule_index.md | 79 ---- .../v52/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v52/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v52/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v53/metadata.json | 5 - .../evolution/history/v53/snapshot/SKILL.md | 61 --- .../history/v53/snapshot/agents/openai.yaml | 4 - .../v53/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v53/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v53/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v53/snapshot/references/layout_and_ui.md | 156 -------- .../v53/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v53/snapshot/references/rule_index.md | 79 ---- .../v53/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v53/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v53/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v54/metadata.json | 5 - .../evolution/history/v54/snapshot/SKILL.md | 61 --- .../history/v54/snapshot/agents/openai.yaml | 4 - .../v54/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v54/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v54/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v54/snapshot/references/layout_and_ui.md | 156 -------- .../v54/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v54/snapshot/references/rule_index.md | 79 ---- .../v54/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v54/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v54/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v55/metadata.json | 5 - .../evolution/history/v55/snapshot/SKILL.md | 61 --- .../history/v55/snapshot/agents/openai.yaml | 4 - .../v55/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v55/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v55/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v55/snapshot/references/layout_and_ui.md | 156 -------- .../v55/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v55/snapshot/references/rule_index.md | 90 ----- .../v55/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v55/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v55/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v56/metadata.json | 5 - .../evolution/history/v56/snapshot/SKILL.md | 61 --- .../history/v56/snapshot/agents/openai.yaml | 4 - .../v56/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v56/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v56/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v56/snapshot/references/layout_and_ui.md | 156 -------- .../v56/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v56/snapshot/references/rule_index.md | 110 ------ .../v56/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v56/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v56/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v57/metadata.json | 5 - .../evolution/history/v57/snapshot/SKILL.md | 67 ---- .../history/v57/snapshot/agents/openai.yaml | 4 - .../v57/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v57/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v57/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v57/snapshot/references/layout_and_ui.md | 156 -------- .../v57/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v57/snapshot/references/rule_index.md | 110 ------ .../v57/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v57/snapshot/references/usage_ledger.md | 181 --------- .../references/validation_scenarios.md | 153 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v57/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v58/metadata.json | 5 - .../evolution/history/v58/snapshot/SKILL.md | 67 ---- .../history/v58/snapshot/agents/openai.yaml | 4 - .../v58/snapshot/references/anti_patterns.md | 234 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 118 ------ .../references/build_release_and_ci.md | 96 ----- .../v58/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v58/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v58/snapshot/references/layout_and_ui.md | 156 -------- .../v58/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v58/snapshot/references/rule_index.md | 110 ------ .../v58/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 157 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v58/snapshot/references/usage_ledger.md | 194 ---------- .../references/validation_scenarios.md | 161 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v58/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v59/metadata.json | 5 - .../evolution/history/v59/snapshot/SKILL.md | 67 ---- .../history/v59/snapshot/agents/openai.yaml | 4 - .../v59/snapshot/references/anti_patterns.md | 235 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 120 ------ .../references/build_release_and_ci.md | 96 ----- .../v59/snapshot/references/code_templates.md | 276 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v59/snapshot/references/examples.md | 143 ------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v59/snapshot/references/layout_and_ui.md | 156 -------- .../v59/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 92 ----- .../references/root_cause_enforcement.md | 117 ------ .../v59/snapshot/references/rule_index.md | 110 ------ .../v59/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 158 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v59/snapshot/references/usage_ledger.md | 194 ---------- .../references/validation_scenarios.md | 161 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v59/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 162 -------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v6/metadata.json | 5 - .../evolution/history/v6/snapshot/SKILL.md | 75 ---- .../history/v6/snapshot/agents/openai.yaml | 4 - .../v6/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v6/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v6/snapshot/references/domain_modeling.md | 96 ----- .../v6/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v6/snapshot/references/layout_and_ui.md | 156 -------- .../v6/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v6/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v6/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v6/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v61/metadata.json | 5 - .../evolution/history/v61/snapshot/SKILL.md | 67 ---- .../history/v61/snapshot/agents/openai.yaml | 4 - .../v61/snapshot/references/anti_patterns.md | 235 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 120 ------ .../references/build_release_and_ci.md | 96 ----- .../v61/snapshot/references/code_templates.md | 277 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v61/snapshot/references/examples.md | 185 --------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v61/snapshot/references/layout_and_ui.md | 156 -------- .../v61/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 103 ----- .../references/root_cause_enforcement.md | 117 ------ .../v61/snapshot/references/rule_index.md | 113 ------ .../v61/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 158 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v61/snapshot/references/usage_ledger.md | 194 ---------- .../references/validation_scenarios.md | 166 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v61/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 208 ---------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v62/metadata.json | 5 - .../evolution/history/v62/snapshot/SKILL.md | 104 ----- .../history/v62/snapshot/agents/openai.yaml | 4 - .../v62/snapshot/references/anti_patterns.md | 235 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 120 ------ .../references/build_release_and_ci.md | 96 ----- .../v62/snapshot/references/code_templates.md | 277 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v62/snapshot/references/examples.md | 185 --------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v62/snapshot/references/layout_and_ui.md | 156 -------- .../v62/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 103 ----- .../references/root_cause_enforcement.md | 117 ------ .../v62/snapshot/references/rule_index.md | 115 ------ .../v62/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 158 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v62/snapshot/references/usage_ledger.md | 194 ---------- .../references/validation_scenarios.md | 166 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v62/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 208 ---------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v63/metadata.json | 5 - .../evolution/history/v63/snapshot/SKILL.md | 104 ----- .../history/v63/snapshot/agents/openai.yaml | 4 - .../v63/snapshot/references/anti_patterns.md | 235 ------------ .../references/architecture_analysis.md | 188 --------- .../references/architecture_and_network.md | 120 ------ .../references/build_release_and_ci.md | 96 ----- .../v63/snapshot/references/code_templates.md | 277 -------------- .../snapshot/references/decision_records.md | 89 ----- .../snapshot/references/domain_modeling.md | 105 ------ .../v63/snapshot/references/examples.md | 185 --------- .../references/execution_playbooks.md | 115 ------ .../snapshot/references/ios_conventions.md | 131 ------- .../v63/snapshot/references/layout_and_ui.md | 156 -------- .../v63/snapshot/references/mcp_control.md | 54 --- .../snapshot/references/migration_strategy.md | 135 ------- .../references/networking_patterns.md | 105 ------ .../references/observability_logging.md | 97 ----- .../references/performance_optimization.md | 69 ---- .../snapshot/references/review_checklists.md | 103 ----- .../references/root_cause_enforcement.md | 127 ------- .../v63/snapshot/references/rule_index.md | 116 ------ .../v63/snapshot/references/self_evolution.md | 145 ------- .../snapshot/references/swift_concurrency.md | 62 --- .../snapshot/references/team_collaboration.md | 55 --- .../references/test_execution_and_repair.md | 100 ----- .../snapshot/references/testing_strategy.md | 158 -------- .../snapshot/references/ui_state_patterns.md | 121 ------ .../v63/snapshot/references/usage_ledger.md | 194 ---------- .../references/validation_scenarios.md | 166 -------- .../snapshot/scripts/append_usage_entry.sh | 149 -------- .../scripts/approve_skill_promotion.sh | 71 ---- .../check_skill_promotion_readiness.sh | 62 --- .../scripts/check_snapshot_consistency.sh | 61 --- .../snapshot/scripts/create_skill_proposal.sh | 53 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../snapshot/scripts/extract_usage_audit.sh | 146 ------- .../scripts/promote_skill_evolution.sh | 108 ------ .../scripts/record_validation_scenario.sh | 115 ------ .../scripts/rollback_skill_evolution.sh | 110 ------ .../scripts/run_behavior_validation.sh | 152 -------- .../scripts/summarize_usage_ledger.sh | 357 ------------------ .../snapshot/scripts/test_proposal_scripts.sh | 113 ------ .../scripts/update_skill_proposal_status.sh | 47 --- .../v63/snapshot/scripts/validate_rule_ids.sh | 163 -------- .../scripts/validate_scenario_specs.sh | 200 ---------- .../scripts/validate_skill_evolution.sh | 208 ---------- .../scripts/validate_skill_proposal.sh | 93 ----- .../snapshot/scripts/validate_usage_ledger.sh | 156 -------- .../evolution/history/v7/metadata.json | 5 - .../evolution/history/v7/snapshot/SKILL.md | 73 ---- .../history/v7/snapshot/agents/openai.yaml | 4 - .../v7/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v7/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v7/snapshot/references/domain_modeling.md | 96 ----- .../v7/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v7/snapshot/references/layout_and_ui.md | 156 -------- .../v7/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v7/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v7/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v7/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v8/metadata.json | 5 - .../evolution/history/v8/snapshot/SKILL.md | 66 ---- .../history/v8/snapshot/agents/openai.yaml | 4 - .../v8/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v8/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v8/snapshot/references/domain_modeling.md | 96 ----- .../v8/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v8/snapshot/references/layout_and_ui.md | 156 -------- .../v8/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v8/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v8/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v8/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../evolution/history/v9/metadata.json | 5 - .../evolution/history/v9/snapshot/SKILL.md | 66 ---- .../history/v9/snapshot/agents/openai.yaml | 4 - .../v9/snapshot/references/anti_patterns.md | 202 ---------- .../references/architecture_and_network.md | 116 ------ .../references/build_release_and_ci.md | 88 ----- .../v9/snapshot/references/code_templates.md | 256 ------------- .../snapshot/references/decision_records.md | 89 ----- .../v9/snapshot/references/domain_modeling.md | 96 ----- .../v9/snapshot/references/examples.md | 164 -------- .../references/execution_playbooks.md | 114 ------ .../v9/snapshot/references/layout_and_ui.md | 156 -------- .../v9/snapshot/references/mcp_control.md | 50 --- .../snapshot/references/migration_strategy.md | 139 ------- .../references/networking_patterns.md | 124 ------ .../references/observability_logging.md | 87 ----- .../references/performance_optimization.md | 73 ---- .../snapshot/references/review_checklists.md | 90 ----- .../references/root_cause_enforcement.md | 111 ------ .../v9/snapshot/references/self_evolution.md | 122 ------ .../snapshot/references/swift_concurrency.md | 61 --- .../v9/snapshot/references/swift_style.md | 50 --- .../snapshot/references/team_collaboration.md | 55 --- .../v9/snapshot/references/terminology.md | 89 ----- .../snapshot/references/test_system_prompt.md | 89 ----- .../snapshot/references/testing_strategy.md | 156 -------- .../snapshot/references/ui_state_patterns.md | 117 ------ .../references/validation_scenarios.md | 148 -------- .../scripts/approve_skill_promotion.sh | 58 --- .../check_skill_promotion_readiness.sh | 57 --- .../snapshot/scripts/create_skill_proposal.sh | 47 --- .../scripts/demo_skill_evolution_flow.sh | 37 -- .../scripts/promote_skill_evolution.sh | 85 ----- .../scripts/record_validation_scenario.sh | 110 ------ .../scripts/rollback_skill_evolution.sh | 40 -- .../scripts/update_skill_proposal_status.sh | 42 --- .../scripts/validate_skill_evolution.sh | 46 --- .../scripts/validate_skill_proposal.sh | 70 ---- .../scripts/gc_evolution_history.sh | 15 + 2506 files changed, 15 insertions(+), 259494 deletions(-) delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/migration_risk_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/refactoring_and_migration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/migration_risk_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/refactoring_and_migration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/migration_risk_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/refactoring_and_migration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/check_snapshot_consistency.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/test_proposal_scripts.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/check_snapshot_consistency.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/test_proposal_scripts.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/check_snapshot_consistency.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/test_proposal_scripts.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/migration_risk_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/refactoring_and_migration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/migration_risk_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/refactoring_and_migration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_analysis.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ios_conventions.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/rule_index.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/test_execution_and_repair.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/usage_ledger.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/validation_scenarios.md delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/append_usage_entry.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/approve_skill_promotion.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/check_snapshot_consistency.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/create_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/extract_usage_audit.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/promote_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/record_validation_scenario.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/rollback_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/run_behavior_validation.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/summarize_usage_ledger.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/test_proposal_scripts.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_rule_ids.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_scenario_specs.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_evolution.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_proposal.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_usage_ledger.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/metadata.json delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/SKILL.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/agents/openai.yaml delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/anti_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/architecture_and_network.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/build_release_and_ci.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/code_templates.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/decision_records.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/domain_modeling.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/examples.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/execution_playbooks.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/layout_and_ui.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/mcp_control.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/migration_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/networking_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/observability_logging.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/performance_optimization.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/review_checklists.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/root_cause_enforcement.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/self_evolution.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_concurrency.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_style.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/team_collaboration.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/terminology.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/test_system_prompt.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/testing_strategy.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/ui_state_patterns.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/validation_scenarios.md delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/approve_skill_promotion.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/check_skill_promotion_readiness.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/create_skill_proposal.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/demo_skill_evolution_flow.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/promote_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/record_validation_scenario.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/rollback_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/update_skill_proposal_status.sh delete mode 100755 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_evolution.sh delete mode 100644 skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_proposal.sh diff --git a/skills-engineering/ios-engineer/evolution/history/v1/metadata.json b/skills-engineering/ios-engineer/evolution/history/v1/metadata.json deleted file mode 100644 index 31377ab..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v1", - "promoted_at": "2026-04-03T00:00:00+08:00", - "source": "manual-bootstrap" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/SKILL.md deleted file mode 100644 index ef1c997..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [refactoring_and_migration.md](references/refactoring_and_migration.md)、[migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态或网络文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md)。 -- 迁移与发布: - 读取 [refactoring_and_migration.md](references/refactoring_and_migration.md),必要时追加 [migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/architecture_and_network.md deleted file mode 100644 index 8139061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,105 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/decision_records.md deleted file mode 100644 index 4f25193..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/decision_records.md +++ /dev/null @@ -1,87 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/domain_modeling.md deleted file mode 100644 index 5cbf37e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,94 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/execution_playbooks.md deleted file mode 100644 index 1e0d20e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,112 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/layout_and_ui.md deleted file mode 100644 index dec5fe9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,84 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/migration_risk_control.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/migration_risk_control.md deleted file mode 100644 index d3d91e1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/migration_risk_control.md +++ /dev/null @@ -1,64 +0,0 @@ -# 迁移风险控制 - -## 目录 -- 使用规则 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 常见反模式 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -- 所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把“建抽象、迁调用、删旧实现”压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写“有问题就回滚”。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 常见反模式 -- 一次性大迁移,不分阶段 -- 没有兼容层就直接切主链路 -- 引入兼容层后无限期不删除 -- 没有灰度,只能全量上线 -- 没有回滚路径就推进重构 -- 发布前没有定义指标和失败信号 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/refactoring_and_migration.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/refactoring_and_migration.md deleted file mode 100644 index c7add04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/refactoring_and_migration.md +++ /dev/null @@ -1,66 +0,0 @@ -# 重构、迁移与代码审查 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- UIKit 与 SwiftUI 混合改造 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性“大爆破”。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把“队列”问题翻译为“隔离域”和“任务层级”问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于“拆文件”而不是“重建边界”。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/review_checklists.md deleted file mode 100644 index bdf1c6c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/review_checklists.md +++ /dev/null @@ -1,88 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d39559b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/self_evolution.md deleted file mode 100644 index 618c0d9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/self_evolution.md +++ /dev/null @@ -1,112 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、历史快照分别存放在 `evolution/proposals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照并更新 active 版本。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证的候选版,才能成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/validation_scenarios.md deleted file mode 100644 index e363c11..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,128 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index 5333d1a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v1/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < " - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause" - exit 1 -fi - -new_version="$1" -source_ref="$2" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json <" - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v11/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v12/metadata.json b/skills-engineering/ios-engineer/evolution/history/v12/metadata.json deleted file mode 100644 index f238fba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v12", - "promoted_at": "2026-04-30T10:51:34+0800", - "source": "proposal:20260430-105026-retire-circular-scope-rule" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v12/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v13/metadata.json b/skills-engineering/ios-engineer/evolution/history/v13/metadata.json deleted file mode 100644 index db64a83..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v13", - "promoted_at": "2026-04-30T11:19:02+0800", - "source": "proposal:20260430-111649-consolidate-antipattern-overlaps" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v13/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v14/metadata.json b/skills-engineering/ios-engineer/evolution/history/v14/metadata.json deleted file mode 100644 index 753cd92..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v14", - "promoted_at": "2026-04-30T11:22:02+0800", - "source": "proposal:20260430-111956-antipattern-verifiable-criteria" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v14/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v15/metadata.json b/skills-engineering/ios-engineer/evolution/history/v15/metadata.json deleted file mode 100644 index 26554c1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v15", - "promoted_at": "2026-04-30T11:25:09+0800", - "source": "proposal:20260430-112243-verifiable-rule-conditions" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v15/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v16/metadata.json b/skills-engineering/ios-engineer/evolution/history/v16/metadata.json deleted file mode 100644 index dde2cb8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v16", - "promoted_at": "2026-04-30T11:27:45+0800", - "source": "proposal:20260430-112554-resolve-cross-file-conflicts" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v16/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v17/metadata.json b/skills-engineering/ios-engineer/evolution/history/v17/metadata.json deleted file mode 100644 index b052c24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v17", - "promoted_at": "2026-04-30T11:35:31+0800", - "source": "proposal:20260430-113427-observability-trigger-condition" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v17/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v18/metadata.json b/skills-engineering/ios-engineer/evolution/history/v18/metadata.json deleted file mode 100644 index 0748713..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v18", - "promoted_at": "2026-04-30T11:37:53+0800", - "source": "proposal:20260430-113630-network-baseline-adaptation" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/SKILL.md deleted file mode 100644 index c675cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/examples.md deleted file mode 100644 index 815df14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/examples.md +++ /dev/null @@ -1,163 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v18/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v19/metadata.json b/skills-engineering/ios-engineer/evolution/history/v19/metadata.json deleted file mode 100644 index d941cc6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v19", - "promoted_at": "2026-04-30T11:46:00+0800", - "source": "proposal:20260430-113828-review-finding-first-exception" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/SKILL.md deleted file mode 100644 index aa71ad6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/examples.md deleted file mode 100644 index 6d2a2e8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/examples.md +++ /dev/null @@ -1,168 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构(**findings-first**,与其他场景的四段式不同;对齐 SKILL.md 核心铁律审查例外): - -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题(按 正确性 → 架构 → 并发 → 性能 → UI → 测试 排序) -1. 问题 1:描述 + 影响 + 修法 -2. ... - -一般问题 -1. ... - -验证缺口 -- 缺哪些测试或验证 - -最终要求 -- 合入前必须完成什么 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 -- 不在审查场景套用"根因 → 为什么 → 修法 → 验证"四段式;findings 本身已包含这些维度。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1933109..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v19/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/metadata.json b/skills-engineering/ios-engineer/evolution/history/v2-drill/metadata.json deleted file mode 100644 index 9b19f9c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v2-drill", - "promoted_at": "2026-04-03T10:01:35+0800", - "source": "proposal:20260403-100130-drill-promotion" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/SKILL.md deleted file mode 100644 index ef1c997..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [refactoring_and_migration.md](references/refactoring_and_migration.md)、[migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态或网络文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md)。 -- 迁移与发布: - 读取 [refactoring_and_migration.md](references/refactoring_and_migration.md),必要时追加 [migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/architecture_and_network.md deleted file mode 100644 index 8139061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,105 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/decision_records.md deleted file mode 100644 index 4f25193..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/decision_records.md +++ /dev/null @@ -1,87 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/domain_modeling.md deleted file mode 100644 index 5cbf37e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,94 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/execution_playbooks.md deleted file mode 100644 index 1e0d20e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,112 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/layout_and_ui.md deleted file mode 100644 index dec5fe9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,84 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/migration_risk_control.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/migration_risk_control.md deleted file mode 100644 index d3d91e1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/migration_risk_control.md +++ /dev/null @@ -1,64 +0,0 @@ -# 迁移风险控制 - -## 目录 -- 使用规则 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 常见反模式 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -- 所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把“建抽象、迁调用、删旧实现”压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写“有问题就回滚”。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 常见反模式 -- 一次性大迁移,不分阶段 -- 没有兼容层就直接切主链路 -- 引入兼容层后无限期不删除 -- 没有灰度,只能全量上线 -- 没有回滚路径就推进重构 -- 发布前没有定义指标和失败信号 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/refactoring_and_migration.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/refactoring_and_migration.md deleted file mode 100644 index c7add04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/refactoring_and_migration.md +++ /dev/null @@ -1,66 +0,0 @@ -# 重构、迁移与代码审查 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- UIKit 与 SwiftUI 混合改造 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性“大爆破”。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把“队列”问题翻译为“隔离域”和“任务层级”问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于“拆文件”而不是“重建边界”。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/review_checklists.md deleted file mode 100644 index bdf1c6c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/review_checklists.md +++ /dev/null @@ -1,88 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d39559b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/self_evolution.md deleted file mode 100644 index 618c0d9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/self_evolution.md +++ /dev/null @@ -1,112 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、历史快照分别存放在 `evolution/proposals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照并更新 active 版本。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证的候选版,才能成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/validation_scenarios.md deleted file mode 100644 index e363c11..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,128 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index 5333d1a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-drill/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < " - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause" - exit 1 -fi - -new_version="$1" -source_ref="$2" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json <" - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [refactoring_and_migration.md](references/refactoring_and_migration.md)、[migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态或网络文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md)。 -- 迁移与发布: - 读取 [refactoring_and_migration.md](references/refactoring_and_migration.md),必要时追加 [migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/architecture_and_network.md deleted file mode 100644 index 8139061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,105 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/decision_records.md deleted file mode 100644 index 4f25193..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/decision_records.md +++ /dev/null @@ -1,87 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/domain_modeling.md deleted file mode 100644 index 5cbf37e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,94 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/execution_playbooks.md deleted file mode 100644 index 1e0d20e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,112 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/layout_and_ui.md deleted file mode 100644 index dec5fe9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,84 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/migration_risk_control.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/migration_risk_control.md deleted file mode 100644 index d3d91e1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/migration_risk_control.md +++ /dev/null @@ -1,64 +0,0 @@ -# 迁移风险控制 - -## 目录 -- 使用规则 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 常见反模式 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -- 所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把“建抽象、迁调用、删旧实现”压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写“有问题就回滚”。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 常见反模式 -- 一次性大迁移,不分阶段 -- 没有兼容层就直接切主链路 -- 引入兼容层后无限期不删除 -- 没有灰度,只能全量上线 -- 没有回滚路径就推进重构 -- 发布前没有定义指标和失败信号 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/refactoring_and_migration.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/refactoring_and_migration.md deleted file mode 100644 index c7add04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/refactoring_and_migration.md +++ /dev/null @@ -1,66 +0,0 @@ -# 重构、迁移与代码审查 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- UIKit 与 SwiftUI 混合改造 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性“大爆破”。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把“队列”问题翻译为“隔离域”和“任务层级”问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于“拆文件”而不是“重建边界”。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/review_checklists.md deleted file mode 100644 index bdf1c6c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/review_checklists.md +++ /dev/null @@ -1,88 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d39559b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/self_evolution.md deleted file mode 100644 index 3fefda0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/self_evolution.md +++ /dev/null @@ -1,114 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证的候选版,才能成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/validation_scenarios.md deleted file mode 100644 index e363c11..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,128 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index 5333d1a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index - -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length - -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 1317225..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2-status-flow/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh " - exit 1 -fi - -proposal_file="$1" -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v2/metadata.json b/skills-engineering/ios-engineer/evolution/history/v2/metadata.json deleted file mode 100644 index 32e012c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v2", - "promoted_at": "2026-04-30T09:50:01+0800", - "source": "proposal:20260430-094412-references-consolidation" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/SKILL.md deleted file mode 100644 index 098e37b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/SKILL.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 当用户询问“当前架构”时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用“代码审查(Code Review)”级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问“当前架构”但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 测试体系与自动修复 -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,先读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明,并放在当前 `class` 的最下面。 -- 变量与方法调用默认使用 `self.` 前缀。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 固定代码结构顺序:`typealias/enum` -> 初始化 -> public API -> private helpers;协议实现放在对应 `extension` 中分组。 -- 命名保持一致:Bool 以 `is/has/can` 前缀;避免含糊缩写;异步/并发相关方法用清晰动词短语表达意图。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称。 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/architecture_and_network.md deleted file mode 100644 index 3ef7a76..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,107 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v2/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v21/metadata.json b/skills-engineering/ios-engineer/evolution/history/v21/metadata.json deleted file mode 100644 index b95fd5f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v21", - "promoted_at": "2026-04-30T11:52:12+0800", - "source": "proposal:20260430-115005-rewrite-unverifiable-no-regression" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/SKILL.md deleted file mode 100644 index aa71ad6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/examples.md deleted file mode 100644 index 6d2a2e8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/examples.md +++ /dev/null @@ -1,168 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构(**findings-first**,与其他场景的四段式不同;对齐 SKILL.md 核心铁律审查例外): - -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题(按 正确性 → 架构 → 并发 → 性能 → UI → 测试 排序) -1. 问题 1:描述 + 影响 + 修法 -2. ... - -一般问题 -1. ... - -验证缺口 -- 缺哪些测试或验证 - -最终要求 -- 合入前必须完成什么 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 -- 不在审查场景套用"根因 → 为什么 → 修法 → 验证"四段式;findings 本身已包含这些维度。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/review_checklists.md deleted file mode 100644 index 48b7f47..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v21/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v22/metadata.json b/skills-engineering/ios-engineer/evolution/history/v22/metadata.json deleted file mode 100644 index 7684594..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v22", - "promoted_at": "2026-04-30T11:55:03+0800", - "source": "proposal:20260430-115355-rewrite-checklist-exhaustive" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/SKILL.md deleted file mode 100644 index aa71ad6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/examples.md deleted file mode 100644 index 6d2a2e8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/examples.md +++ /dev/null @@ -1,168 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构(**findings-first**,与其他场景的四段式不同;对齐 SKILL.md 核心铁律审查例外): - -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题(按 正确性 → 架构 → 并发 → 性能 → UI → 测试 排序) -1. 问题 1:描述 + 影响 + 修法 -2. ... - -一般问题 -1. ... - -验证缺口 -- 缺哪些测试或验证 - -最终要求 -- 合入前必须完成什么 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 -- 不在审查场景套用"根因 → 为什么 → 修法 → 验证"四段式;findings 本身已包含这些维度。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/mcp_control.md deleted file mode 100644 index b49a42a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/review_checklists.md deleted file mode 100644 index 0f9b39d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v22/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v23/metadata.json b/skills-engineering/ios-engineer/evolution/history/v23/metadata.json deleted file mode 100644 index d6e2eda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v23", - "promoted_at": "2026-04-30T11:57:10+0800", - "source": "proposal:20260430-115553-retire-hard-tool-budget-count" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/SKILL.md deleted file mode 100644 index aa71ad6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 审查结论 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/examples.md deleted file mode 100644 index 6d2a2e8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/examples.md +++ /dev/null @@ -1,168 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构(**findings-first**,与其他场景的四段式不同;对齐 SKILL.md 核心铁律审查例外): - -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题(按 正确性 → 架构 → 并发 → 性能 → UI → 测试 排序) -1. 问题 1:描述 + 影响 + 修法 -2. ... - -一般问题 -1. ... - -验证缺口 -- 缺哪些测试或验证 - -最终要求 -- 合入前必须完成什么 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 -- 不在审查场景套用"根因 → 为什么 → 修法 → 验证"四段式;findings 本身已包含这些维度。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/review_checklists.md deleted file mode 100644 index 0f9b39d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v23/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v24/metadata.json b/skills-engineering/ios-engineer/evolution/history/v24/metadata.json deleted file mode 100644 index 48bbb08..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v24", - "promoted_at": "2026-04-30T14:18:18+0800", - "source": "proposal:20260430-141450-review-findings-first-consistency" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/architecture_and_network.md deleted file mode 100644 index 76e2e68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/domain_modeling.md deleted file mode 100644 index 16f0ca7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / URLSession 层捕获,转为 `ErrorModel.network`,不向上暴露 `NSError`。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/examples.md deleted file mode 100644 index 6d2a2e8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/examples.md +++ /dev/null @@ -1,168 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构(**findings-first**,与其他场景的四段式不同;对齐 SKILL.md 核心铁律审查例外): - -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题(按 正确性 → 架构 → 并发 → 性能 → UI → 测试 排序) -1. 问题 1:描述 + 影响 + 修法 -2. ... - -一般问题 -1. ... - -验证缺口 -- 缺哪些测试或验证 - -最终要求 -- 合入前必须完成什么 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 -- 不在审查场景套用"根因 → 为什么 → 修法 → 验证"四段式;findings 本身已包含这些维度。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/networking_patterns.md deleted file mode 100644 index 034703e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,115 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v24/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v25/metadata.json b/skills-engineering/ios-engineer/evolution/history/v25/metadata.json deleted file mode 100644 index b4fe962..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v25", - "promoted_at": "2026-04-30T14:29:14+0800", - "source": "proposal:20260430-142606-post-Q-T-U-cross-file-cleanup" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/architecture_and_network.md deleted file mode 100644 index c729178..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,129 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/self_evolution.md deleted file mode 100644 index 1239c8c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/self_evolution.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v25/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v26/metadata.json b/skills-engineering/ios-engineer/evolution/history/v26/metadata.json deleted file mode 100644 index 4b1b686..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v26", - "promoted_at": "2026-04-30T14:33:18+0800", - "source": "proposal:20260430-143130-enforce-cross-file-grep" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/architecture_and_network.md deleted file mode 100644 index c729178..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,129 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/self_evolution.md deleted file mode 100644 index 5898a25..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/self_evolution.md +++ /dev/null @@ -1,125 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v26/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v27/metadata.json b/skills-engineering/ios-engineer/evolution/history/v27/metadata.json deleted file mode 100644 index fe1f022..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v27", - "promoted_at": "2026-04-30T14:43:43+0800", - "source": "proposal:20260430-144213-unify-network-pattern-ownership" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/architecture_and_network.md deleted file mode 100644 index b90a6b9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 网络模式完整定义(链路职责 / 分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 错误分层 / 常见反模式)全部在 [networking_patterns.md](networking_patterns.md)。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/performance_optimization.md deleted file mode 100644 index 2e7f2cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/self_evolution.md deleted file mode 100644 index 5898a25..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/self_evolution.md +++ /dev/null @@ -1,125 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v27/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v28/metadata.json b/skills-engineering/ios-engineer/evolution/history/v28/metadata.json deleted file mode 100644 index 4dda6d6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v28", - "promoted_at": "2026-04-30T14:46:10+0800", - "source": "proposal:20260430-144416-unify-performance-metric-ownership" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/architecture_and_network.md deleted file mode 100644 index b90a6b9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 网络模式完整定义(链路职责 / 分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 错误分层 / 常见反模式)全部在 [networking_patterns.md](networking_patterns.md)。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/observability_logging.md deleted file mode 100644 index 730e806..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/self_evolution.md deleted file mode 100644 index 5898a25..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/self_evolution.md +++ /dev/null @@ -1,125 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v28/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v29/metadata.json b/skills-engineering/ios-engineer/evolution/history/v29/metadata.json deleted file mode 100644 index 4098aa5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v29", - "promoted_at": "2026-04-30T14:56:40+0800", - "source": "proposal:20260430-145330-fix-cross-file-references-DD" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/SKILL.md deleted file mode 100644 index 68d02bf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格问题追加 [swift_style.md](references/swift_style.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码风格 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [swift_style.md](references/swift_style.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/mcp_control.md deleted file mode 100644 index b6e3d35..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/mcp_control.md +++ /dev/null @@ -1,46 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/self_evolution.md deleted file mode 100644 index 5898a25..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/self_evolution.md +++ /dev/null @@ -1,125 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v29/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v3/metadata.json b/skills-engineering/ios-engineer/evolution/history/v3/metadata.json deleted file mode 100644 index 65259b5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v3", - "promoted_at": "2026-04-30T09:59:26+0800", - "source": "proposal:20260430-095705-retire-duplicate-constraints" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/SKILL.md deleted file mode 100644 index 0e9be11..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 当用户询问“当前架构”时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用“代码审查(Code Review)”级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问“当前架构”但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 测试体系与自动修复 -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,先读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明,并放在当前 `class` 的最下面。 -- 变量与方法调用默认使用 `self.` 前缀。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 固定代码结构顺序:`typealias/enum` -> 初始化 -> public API -> private helpers;协议实现放在对应 `extension` 中分组。 -- 命名保持一致:Bool 以 `is/has/can` 前缀;避免含糊缩写;异步/并发相关方法用清晰动词短语表达意图。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称。 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/architecture_and_network.md deleted file mode 100644 index 3ef7a76..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,107 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v3/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v31/metadata.json b/skills-engineering/ios-engineer/evolution/history/v31/metadata.json deleted file mode 100644 index fe14d1f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v31", - "promoted_at": "2026-04-30T16:26:50+0800", - "source": "proposal:20260430-161410-batch-script-rule-hardening" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/SKILL.md deleted file mode 100644 index e80624e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/code_templates.md deleted file mode 100644 index 6782277..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/code_templates.md +++ /dev/null @@ -1,272 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ios_conventions.md deleted file mode 100644 index 8f28916..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f7da036..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100644 index fe0b30a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 8756434..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/7] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/7] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/7] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/7] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/7] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/7] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/7] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 98f3144..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v31/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v32/metadata.json b/skills-engineering/ios-engineer/evolution/history/v32/metadata.json deleted file mode 100644 index b99e89b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v32", - "promoted_at": "2026-04-30T16:55:44+0800", - "source": "proposal:20260430-165137-fix-v31-drift-and-script-hardening" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/SKILL.md deleted file mode 100644 index e80624e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/code_templates.md deleted file mode 100644 index 5f5c62d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/code_templates.md +++ /dev/null @@ -1,275 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f7da036..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100644 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 8756434..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/7] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/7] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/7] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/7] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/7] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/7] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/7] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 98f3144..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v32/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v33/metadata.json b/skills-engineering/ios-engineer/evolution/history/v33/metadata.json deleted file mode 100644 index acd24ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v33", - "promoted_at": "2026-04-30T17:14:53+0800", - "source": "proposal:20260430-171045-harden-evolution-flow-and-tests" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/SKILL.md deleted file mode 100644 index e80624e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index b45a2f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100644 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 4b2608c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/8] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/8] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/8] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/8] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/8] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/8] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/8] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/8] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v33/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v34/metadata.json b/skills-engineering/ios-engineer/evolution/history/v34/metadata.json deleted file mode 100644 index 692d05d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v34", - "promoted_at": "2026-04-30T17:22:12+0800", - "source": "proposal:20260430-171802-add-behavior-validation-layer" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/SKILL.md deleted file mode 100644 index e80624e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/run_behavior_validation.sh deleted file mode 100644 index 9cb9ce0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/3] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/3] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/3] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index ea3e49d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100644 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v34/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v35/metadata.json b/skills-engineering/ios-engineer/evolution/history/v35/metadata.json deleted file mode 100644 index 0f92662..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v35", - "promoted_at": "2026-04-30T17:27:34+0800", - "source": "proposal:20260430-172538-add-real-task-behavior-scenarios" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/SKILL.md deleted file mode 100644 index e80624e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/create_skill_proposal.sh deleted file mode 100644 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/run_behavior_validation.sh deleted file mode 100644 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index ea3e49d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100644 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v35/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v36/metadata.json b/skills-engineering/ios-engineer/evolution/history/v36/metadata.json deleted file mode 100644 index 663d180..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v36", - "promoted_at": "2026-05-08T10:41:27+0800", - "source": "proposal:20260508-103117-consolidate-ios-test-execution-reference" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/SKILL.md deleted file mode 100644 index 1f947b0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 14e0c99..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,96 +0,0 @@ -# 测试执行与失败修复 - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按本流程执行。 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index ea3e49d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v36/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v37/metadata.json b/skills-engineering/ios-engineer/evolution/history/v37/metadata.json deleted file mode 100644 index a54d069..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v37", - "promoted_at": "2026-05-08T10:43:46+0800", - "source": "proposal:20260508-104200-scripts-exec-bit-and-guard" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/SKILL.md deleted file mode 100644 index 1f947b0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 901f57f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 14e0c99..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,96 +0,0 @@ -# 测试执行与失败修复 - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按本流程执行。 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v37/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v38/metadata.json b/skills-engineering/ios-engineer/evolution/history/v38/metadata.json deleted file mode 100644 index 0ffaf02..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v38", - "promoted_at": "2026-05-08T10:50:39+0800", - "source": "proposal:20260508-104821-add-usage-section-to-root-cause-and-test-exec" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/SKILL.md deleted file mode 100644 index 1f947b0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 结构输出(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求),详见 [review_checklists.md](references/review_checklists.md)。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 的 findings-first 标准输出骨架(审查结论 / 严重问题 / 一般问题 / 验证缺口 / 最终要求)。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/migration_strategy.md deleted file mode 100644 index 1b1d257..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 的 findings-first 标准输出骨架;迁移相关问题在"严重问题 / 一般问题"中按上述额外检查项命中与否分类。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/self_evolution.md deleted file mode 100644 index ed87868..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出 / findings-first 骨架 / 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 5961d53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v38/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v39/metadata.json b/skills-engineering/ios-engineer/evolution/history/v39/metadata.json deleted file mode 100644 index e90dee3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v39", - "promoted_at": "2026-05-08T10:56:08+0800", - "source": "proposal:20260508-105236-consolidate-output-template-owners" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/SKILL.md deleted file mode 100644 index 39555f2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ios_conventions.md deleted file mode 100644 index 5135006..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,130 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/self_evolution.md deleted file mode 100644 index 163eff3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 2f4be33..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论\s*\n[^\n]*不可合入[^\n]*可合入[\s\S]*?严重问题[\s\S]*?一般问题[\s\S]*?验证缺口[\s\S]*?最终要求/m => ['review_checklists.md', 'findings-first 完整骨架定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - Dir.glob('references/*.md').sort.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - Dir.glob('references/*.md').sort.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v39/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/metadata.json b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/metadata.json deleted file mode 100644 index cad3aa7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v4-approved-drill", - "promoted_at": "2026-04-03T10:22:11+0800", - "source": "proposal:20260403-100527-drill-scenario-record" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/SKILL.md deleted file mode 100644 index ef1c997..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [refactoring_and_migration.md](references/refactoring_and_migration.md)、[migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态或网络文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md)。 -- 迁移与发布: - 读取 [refactoring_and_migration.md](references/refactoring_and_migration.md),必要时追加 [migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/architecture_and_network.md deleted file mode 100644 index 8139061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,105 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/decision_records.md deleted file mode 100644 index 4f25193..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/decision_records.md +++ /dev/null @@ -1,87 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/domain_modeling.md deleted file mode 100644 index 5cbf37e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,94 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/execution_playbooks.md deleted file mode 100644 index 1e0d20e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,112 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/layout_and_ui.md deleted file mode 100644 index dec5fe9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,84 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/migration_risk_control.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/migration_risk_control.md deleted file mode 100644 index d3d91e1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/migration_risk_control.md +++ /dev/null @@ -1,64 +0,0 @@ -# 迁移风险控制 - -## 目录 -- 使用规则 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 常见反模式 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -- 所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把“建抽象、迁调用、删旧实现”压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写“有问题就回滚”。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 常见反模式 -- 一次性大迁移,不分阶段 -- 没有兼容层就直接切主链路 -- 引入兼容层后无限期不删除 -- 没有灰度,只能全量上线 -- 没有回滚路径就推进重构 -- 发布前没有定义指标和失败信号 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/refactoring_and_migration.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/refactoring_and_migration.md deleted file mode 100644 index c7add04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/refactoring_and_migration.md +++ /dev/null @@ -1,66 +0,0 @@ -# 重构、迁移与代码审查 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- UIKit 与 SwiftUI 混合改造 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性“大爆破”。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把“队列”问题翻译为“隔离域”和“任务层级”问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于“拆文件”而不是“重建边界”。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/review_checklists.md deleted file mode 100644 index bdf1c6c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/review_checklists.md +++ /dev/null @@ -1,88 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d39559b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/self_evolution.md deleted file mode 100644 index 8ff0515..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/self_evolution.md +++ /dev/null @@ -1,121 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "ready_to_promote" ]; then - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index 816c287..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index - -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length - -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4-approved-drill/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v4/metadata.json b/skills-engineering/ios-engineer/evolution/history/v4/metadata.json deleted file mode 100644 index b31ac0e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v4", - "promoted_at": "2026-04-30T10:05:14+0800", - "source": "proposal:20260430-100302-downshift-swift-style" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/SKILL.md deleted file mode 100644 index c018d67..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/SKILL.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 当用户询问“当前架构”时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用“代码审查(Code Review)”级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问“当前架构”但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 测试体系与自动修复 -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,先读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/architecture_and_network.md deleted file mode 100644 index 3ef7a76..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,107 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v4/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v41/metadata.json b/skills-engineering/ios-engineer/evolution/history/v41/metadata.json deleted file mode 100644 index fbb0172..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v41", - "promoted_at": "2026-05-08T11:11:16+0800", - "source": "proposal:20260508-110854-require-version-baseline-confirmation" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/SKILL.md deleted file mode 100644 index be3f69a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/self_evolution.md deleted file mode 100644 index 163eff3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index fc7db2e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v41/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v42/metadata.json b/skills-engineering/ios-engineer/evolution/history/v42/metadata.json deleted file mode 100644 index a5085f7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v42", - "promoted_at": "2026-05-08T11:17:27+0800", - "source": "proposal:20260508-111230-add-pre-commit-proposal-binding-hook" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/SKILL.md deleted file mode 100644 index be3f69a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/self_evolution.md deleted file mode 100644 index 163eff3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index fc7db2e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/9] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/9] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/9] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/9] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/9] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/9] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[7/9] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[8/9] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[9/9] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v42/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v43/metadata.json b/skills-engineering/ios-engineer/evolution/history/v43/metadata.json deleted file mode 100644 index d454828..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v43", - "promoted_at": "2026-05-08T11:49:05+0800", - "source": "proposal:20260508-113308-bootstrap-scenario-specs" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/SKILL.md deleted file mode 100644 index be3f69a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/self_evolution.md deleted file mode 100644 index 628f352..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/self_evolution.md +++ /dev/null @@ -1,127 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/validation_scenarios.md deleted file mode 100644 index 769b509..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,149 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 81c483f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("| 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) |") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 9459b34..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 9a8b8a5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/10] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/10] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/10] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/10] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/10] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/10] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/10] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[8/10] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[9/10] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[10/10] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v43/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v44/metadata.json b/skills-engineering/ios-engineer/evolution/history/v44/metadata.json deleted file mode 100644 index 803d5cf..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v44", - "promoted_at": "2026-05-08T14:29:28+0800", - "source": "proposal:20260508-141100-bootstrap-rule-ids" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/SKILL.md deleted file mode 100644 index 2f56375..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- [IR-009] 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 架构分析 / 架构体检 / 项目健康度 / 技术债盘点 / 系统性风险 / 当前架构有没有问题 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- [ROUTE-019] **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/rule_index.md deleted file mode 100644 index bfe1dc6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/rule_index.md +++ /dev/null @@ -1,80 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | -| IR-009 | active | 统一遵守 [ios_conventions.md](ios_conventions.md) | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 架构分析 / 项目健康度 / 技术债盘点 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 → self_evolution.md | 同上 | -| ROUTE-019 | active | Skill 验证场景 → validation_scenarios.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| (暂无) | | | | | diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/self_evolution.md deleted file mode 100644 index 7cf7f15..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/self_evolution.md +++ /dev/null @@ -1,136 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index df9881f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/11] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/11] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/11] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/11] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/11] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/11] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/11] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/11] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[9/11] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[10/11] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[11/11] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v44/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v45/metadata.json b/skills-engineering/ios-engineer/evolution/history/v45/metadata.json deleted file mode 100644 index f6f026b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v45", - "promoted_at": "2026-05-08T14:56:56+0800", - "source": "proposal:20260508-145208-rewrite-sym-007-as-symptom" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/SKILL.md deleted file mode 100644 index 6a91e38..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- [IR-009] 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- [ROUTE-019] **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/rule_index.md deleted file mode 100644 index b17ee4e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/rule_index.md +++ /dev/null @@ -1,80 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | -| IR-009 | active | 统一遵守 [ios_conventions.md](ios_conventions.md) | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 → self_evolution.md | 同上 | -| ROUTE-019 | active | Skill 验证场景 → validation_scenarios.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| (暂无) | | | | | diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/self_evolution.md deleted file mode 100644 index 7cf7f15..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/self_evolution.md +++ /dev/null @@ -1,136 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index df9881f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/11] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/11] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/11] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/11] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/11] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/11] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/11] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/11] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[9/11] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[10/11] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[11/11] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v45/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v46/metadata.json b/skills-engineering/ios-engineer/evolution/history/v46/metadata.json deleted file mode 100644 index 90fe58e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v46", - "promoted_at": "2026-05-08T15:04:27+0800", - "source": "proposal:20260508-143545-bootstrap-usage-ledger" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/SKILL.md deleted file mode 100644 index 6a91e38..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- [IR-009] 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- [ROUTE-019] **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/rule_index.md deleted file mode 100644 index b17ee4e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/rule_index.md +++ /dev/null @@ -1,80 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | -| IR-009 | active | 统一遵守 [ios_conventions.md](ios_conventions.md) | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 → self_evolution.md | 同上 | -| ROUTE-019 | active | Skill 验证场景 → validation_scenarios.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| (暂无) | | | | | diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/self_evolution.md deleted file mode 100644 index 648fcb4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/self_evolution.md +++ /dev/null @@ -1,144 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v46/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/metadata.json b/skills-engineering/ios-engineer/evolution/history/v47/metadata.json deleted file mode 100644 index 8fd2404..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v47", - "promoted_at": "2026-05-08T15:24:19+0800", - "source": "proposal:20260508-151354-bootstrap-summarize-usage-ledger" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/SKILL.md deleted file mode 100644 index 6a91e38..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/SKILL.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- [IR-009] 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役**:主读 [self_evolution.md](references/self_evolution.md);需要验证场景追加 [validation_scenarios.md](references/validation_scenarios.md)。 -- [ROUTE-019] **Skill 验证场景**:主读 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/rule_index.md deleted file mode 100644 index b17ee4e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/rule_index.md +++ /dev/null @@ -1,80 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | -| IR-009 | active | 统一遵守 [ios_conventions.md](ios_conventions.md) | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 → self_evolution.md | 同上 | -| ROUTE-019 | active | Skill 验证场景 → validation_scenarios.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| (暂无) | | | | | diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v47/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/metadata.json b/skills-engineering/ios-engineer/evolution/history/v48/metadata.json deleted file mode 100644 index 733630a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v48", - "promoted_at": "2026-05-08T15:46:52+0800", - "source": "proposal:20260508-154338-retire-route-019-merge-into-018" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/SKILL.md deleted file mode 100644 index fc3c0b7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/SKILL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 -- [IR-009] 统一遵守 [ios_conventions.md](references/ios_conventions.md)。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/rule_index.md deleted file mode 100644 index 34df8ab..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | -| IR-009 | active | 统一遵守 [ios_conventions.md](ios_conventions.md) | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v48/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/metadata.json b/skills-engineering/ios-engineer/evolution/history/v49/metadata.json deleted file mode 100644 index 8c9bd6e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v49", - "promoted_at": "2026-05-08T15:53:22+0800", - "source": "proposal:20260508-155152-retire-ir-009-meta-ir" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/SKILL.md deleted file mode 100644 index a710034..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能观测 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/rule_index.md deleted file mode 100644 index 663f8d8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v49/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/metadata.json b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/metadata.json deleted file mode 100644 index 448a646..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v5-demo-flow", - "promoted_at": "2026-04-03T10:32:11+0800", - "source": "proposal:20260403-103002-demo-full-flow" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/SKILL.md deleted file mode 100644 index ef1c997..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/SKILL.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [refactoring_and_migration.md](references/refactoring_and_migration.md)、[migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 - -## 首步分流 -先把任务归入一个主类,再只读取该主类对应文档;若命中高风险门禁,再追加附加文档。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态或网络文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md)。 -- 迁移与发布: - 读取 [refactoring_and_migration.md](references/refactoring_and_migration.md),必要时追加 [migration_risk_control.md](references/migration_risk_control.md)、[build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 执行流程 -1. 先取证:确认现象、触发条件、影响范围和已知事实。 -2. 再定边界:明确责任层、状态归属、依赖方向和改动边界。 -3. 再实现或裁决:给最小修复或最小可演进方案。 -4. 最后验证:说明验证路径、未覆盖风险和副作用。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格区分 DTO、Entity、ViewState、ErrorModel,不让传输模型或底层错误直接泄露到 UI。 -- 严格回答异步流程的四个问题:谁创建、谁持有、谁取消、何时释放。 -- 严格控制页面状态机、列表状态、表单状态和异步回写,不用多个布尔值拼状态。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 新增字段、参数或状态若依赖上游透传,必须沿完整调用链补齐数据来源、映射、构造和传递路径;不得只在消费端声明变量、追加参数或做局部占位使当前文件先通过编译。 -- 严格执行网络边界、缓存、重试、鉴权、错误分层和幂等语义。 -- 严格补齐日志、埋点、性能观测和排障取证链路。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 涉及并发修复时,明确隔离策略、取消策略、过期结果处理和验证方法。 -- 涉及迁移时,明确阶段计划、兼容层、灰度范围、失败信号和回滚路径。 -- 涉及发布或 CI 风险时,明确构建配置、依赖来源、门禁条件和发布观测项。 -- 涉及性能优化时,明确基线指标、优化动作和优化后对比。 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 参考资料加载规则 -- 默认只读取当前任务直接相关的 2 到 4 份参考资料;不要先通读全部文档。 -- 若任务命中高风险门禁文档,例如测试策略、迁移风险、构建发布、MCP 控制或团队协作规则,允许超出 4 份,但必须先区分主文档和附加门禁文档。 -- 当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -## 快速检查 -- [ ] 是否已经定义清楚边界、依赖方向和状态归属? -- [ ] 是否已经定位根因,而不是只修表象? -- [ ] 是否已经说明任务创建、持有、取消和释放关系? -- [ ] 是否已经避免 DTO、底层错误或共享可变状态向上泄露? -- [ ] 是否已经补齐测试策略、观测信号和残留风险? -- [ ] 如果是迁移或发布相关改动,是否已经定义灰度和回滚? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/architecture_and_network.md deleted file mode 100644 index 8139061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,105 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/decision_records.md deleted file mode 100644 index 4f25193..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/decision_records.md +++ /dev/null @@ -1,87 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/domain_modeling.md deleted file mode 100644 index 5cbf37e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,94 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/execution_playbooks.md deleted file mode 100644 index 1e0d20e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,112 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/layout_and_ui.md deleted file mode 100644 index dec5fe9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,84 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/migration_risk_control.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/migration_risk_control.md deleted file mode 100644 index d3d91e1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/migration_risk_control.md +++ /dev/null @@ -1,64 +0,0 @@ -# 迁移风险控制 - -## 目录 -- 使用规则 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 常见反模式 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -- 所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把“建抽象、迁调用、删旧实现”压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写“有问题就回滚”。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 常见反模式 -- 一次性大迁移,不分阶段 -- 没有兼容层就直接切主链路 -- 引入兼容层后无限期不删除 -- 没有灰度,只能全量上线 -- 没有回滚路径就推进重构 -- 发布前没有定义指标和失败信号 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/refactoring_and_migration.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/refactoring_and_migration.md deleted file mode 100644 index c7add04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/refactoring_and_migration.md +++ /dev/null @@ -1,66 +0,0 @@ -# 重构、迁移与代码审查 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- UIKit 与 SwiftUI 混合改造 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性“大爆破”。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把“队列”问题翻译为“隔离域”和“任务层级”问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于“拆文件”而不是“重建边界”。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/review_checklists.md deleted file mode 100644 index bdf1c6c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/review_checklists.md +++ /dev/null @@ -1,88 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d39559b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,109 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/self_evolution.md deleted file mode 100644 index 8ff0515..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/self_evolution.md +++ /dev/null @@ -1,121 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index 4627415..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index - -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length - -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5-demo-flow/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v5/metadata.json b/skills-engineering/ios-engineer/evolution/history/v5/metadata.json deleted file mode 100644 index 48344e2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v5", - "promoted_at": "2026-04-30T10:07:34+0800", - "source": "proposal:20260430-100521-consolidate-overlapping-sections" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/SKILL.md deleted file mode 100644 index 6700117..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/SKILL.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 当用户询问“当前架构”时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用“代码审查(Code Review)”级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问“当前架构”但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 -- 需要构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 首步分流 -先把任务归入一个主类,默认只读取该主类对应的 2 到 4 份文档;若命中高风险门禁再追加附加文档。当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/architecture_and_network.md deleted file mode 100644 index 3ef7a76..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,107 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v5/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v51/metadata.json b/skills-engineering/ios-engineer/evolution/history/v51/metadata.json deleted file mode 100644 index edd9c04..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v51", - "promoted_at": "2026-05-08T15:58:42+0800", - "source": "proposal:20260508-155553-tighten-route-012-refactor-as-execution" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/SKILL.md deleted file mode 100644 index 5a8f807..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本(接手遗留页 / 排查偶现 Crash / 性能优化 / 并发迁移 / 大型重构)**:先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/rule_index.md deleted file mode 100644 index b1617b5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v51/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/metadata.json b/skills-engineering/ios-engineer/evolution/history/v52/metadata.json deleted file mode 100644 index dcb97be..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v52", - "promoted_at": "2026-05-08T16:01:37+0800", - "source": "proposal:20260508-155946-tighten-route-017-playbook-entry-condition" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/SKILL.md deleted file mode 100644 index 67d2e7b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(需满足"跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地"至少一项才走剧本;否则走 SYM 与 ROUTE 单点路由):剧本涵盖 接手遗留页 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:使用 [review_checklists.md](references/review_checklists.md) 第 8 节的 findings-first 标准输出骨架。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/rule_index.md deleted file mode 100644 index b1617b5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review 使用 review_checklists.md 第 8 节 findings-first 骨架 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v52/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/metadata.json b/skills-engineering/ios-engineer/evolution/history/v53/metadata.json deleted file mode 100644 index 13cb33f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v53", - "promoted_at": "2026-05-08T16:06:07+0800", - "source": "proposal:20260508-160250-compress-out-002-cross-ref-ir-004" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/SKILL.md deleted file mode 100644 index c0e8efe..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(需满足"跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地"至少一项才走剧本;否则走 SYM 与 ROUTE 单点路由):剧本涵盖 接手遗留页 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/execution_playbooks.md deleted file mode 100644 index 761197f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/rule_index.md deleted file mode 100644 index 34bd837..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v53/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/metadata.json b/skills-engineering/ios-engineer/evolution/history/v54/metadata.json deleted file mode 100644 index cc56683..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v54", - "promoted_at": "2026-05-08T16:25:25+0800", - "source": "proposal:20260508-162159-align-playbook-headings-with-route-017" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/SKILL.md deleted file mode 100644 index 5f18c47..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(需满足"跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地"至少一项才走剧本;否则走 SYM 与 ROUTE 单点路由):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/rule_index.md deleted file mode 100644 index 34bd837..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/rule_index.md +++ /dev/null @@ -1,79 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v54/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/metadata.json b/skills-engineering/ios-engineer/evolution/history/v55/metadata.json deleted file mode 100644 index c891413..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v55", - "promoted_at": "2026-05-08T18:26:48+0800", - "source": "proposal:20260508-182458-add-cross-ref-index-for-shared-concepts" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/SKILL.md deleted file mode 100644 index 5f18c47..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(需满足"跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地"至少一项才走剧本;否则走 SYM 与 ROUTE 单点路由):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/rule_index.md deleted file mode 100644 index 31bc518..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/rule_index.md +++ /dev/null @@ -1,90 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v55/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/metadata.json b/skills-engineering/ios-engineer/evolution/history/v56/metadata.json deleted file mode 100644 index d868c1a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v56", - "promoted_at": "2026-05-08T18:28:33+0800", - "source": "proposal:20260508-182705-add-out-subunit-mapping-table" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/SKILL.md deleted file mode 100644 index 5f18c47..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/SKILL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(需满足"跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地"至少一项才走剧本;否则走 SYM 与 ROUTE 单点路由):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/rule_index.md deleted file mode 100644 index 95f7cf0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/rule_index.md +++ /dev/null @@ -1,110 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本 → execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v56/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/metadata.json b/skills-engineering/ios-engineer/evolution/history/v57/metadata.json deleted file mode 100644 index 9b3a0d1..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v57", - "promoted_at": "2026-05-08T18:30:13+0800", - "source": "proposal:20260508-182847-clarify-sym-vs-playbook-routing-precedence" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/SKILL.md deleted file mode 100644 index 6b3b0b4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/SKILL.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/rule_index.md deleted file mode 100644 index 6f27cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/rule_index.md +++ /dev/null @@ -1,110 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/usage_ledger.md deleted file mode 100644 index 0245291..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,181 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/validation_scenarios.md deleted file mode 100644 index 30db84b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,153 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v57/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/metadata.json b/skills-engineering/ios-engineer/evolution/history/v58/metadata.json deleted file mode 100644 index f6d6706..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v58", - "promoted_at": "2026-05-08T18:32:08+0800", - "source": "proposal:20260508-183039-document-meta-sync-protocol-and-signal-thresholds" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/SKILL.md deleted file mode 100644 index 6b3b0b4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/SKILL.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/anti_patterns.md deleted file mode 100644 index 0b9cbe7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,234 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_and_network.md deleted file mode 100644 index ff5928c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,118 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/rule_index.md deleted file mode 100644 index 6f27cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/rule_index.md +++ /dev/null @@ -1,110 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/testing_strategy.md deleted file mode 100644 index 90844ca..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,157 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/usage_ledger.md deleted file mode 100644 index 792af37..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,194 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 提案候选信号阈值 - -[scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 硬编码 4 个阈值常量,超过即在 summarize 输出中作为提案候选信号浮出。本节是这 4 个常量的文档化镜像: - -| 常量 | 值 | 候选提案信号 | 含义 | -|------|----|-------------|------| -| `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | -| `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | -| `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | - -**漂移防护**:阈值与 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 的 `*_THRESHOLD` 常量一一对应。改本文必须同时改脚本,否则 summarize 输出(`thresholds` 字段会带脚本真值)与文档解释会漂移。后续提案可考虑把"脚本常量 ↔ 本表数字"双向校验补到 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh)。 - -## 9. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/validation_scenarios.md deleted file mode 100644 index 31d216a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,161 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -### JSON 与本文同步流程 -按以下顺序执行,跳步会被对应脚本捕获: - -1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 -3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v58/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/metadata.json b/skills-engineering/ios-engineer/evolution/history/v59/metadata.json deleted file mode 100644 index 3cd7651..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v59", - "promoted_at": "2026-05-08T18:39:42+0800", - "source": "proposal:20260508-183824-add-bidirectional-owner-boundary-statements" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/SKILL.md deleted file mode 100644 index 6b3b0b4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/SKILL.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,输出前必须先从工程读取 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议,应先向用户或工程文件求证。本 skill 不预设默认基线。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/anti_patterns.md deleted file mode 100644 index d5b8095..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,235 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 -- 本文件是**反模式库**(识别条件 / 风险 / 修法)。**审查检查表与可合入判定**归 [review_checklists.md](review_checklists.md);二者配合使用——审查时先按 review_checklists.md 维度过检,命中时回查本文件对应反模式条目。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_and_network.md deleted file mode 100644 index 17b0f90..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,120 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -本文件承担**实施类**架构设计与改造写法。**评估类**输出(架构体检 / 健康度评分 / 系统性风险排查 / 重构路线图)归 [architecture_analysis.md](architecture_analysis.md)。 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/code_templates.md deleted file mode 100644 index 183eca9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/code_templates.md +++ /dev/null @@ -1,276 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/examples.md deleted file mode 100644 index 6d19985..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/examples.md +++ /dev/null @@ -1,143 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/review_checklists.md deleted file mode 100644 index bbbbe53..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/review_checklists.md +++ /dev/null @@ -1,92 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/rule_index.md deleted file mode 100644 index 6f27cd3..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/rule_index.md +++ /dev/null @@ -1,110 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的建议,输出前必须先求证 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/testing_strategy.md deleted file mode 100644 index 29b22a2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,158 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件承担**测试规划**(层次划分 / 覆盖策略 / stub 设计)。**测试执行与失败修复**(跑测试 / 分析失败 / 平台验证排查)归 [test_execution_and_repair.md](test_execution_and_repair.md)。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/usage_ledger.md deleted file mode 100644 index 792af37..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,194 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 提案候选信号阈值 - -[scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 硬编码 4 个阈值常量,超过即在 summarize 输出中作为提案候选信号浮出。本节是这 4 个常量的文档化镜像: - -| 常量 | 值 | 候选提案信号 | 含义 | -|------|----|-------------|------| -| `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | -| `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | -| `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | - -**漂移防护**:阈值与 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 的 `*_THRESHOLD` 常量一一对应。改本文必须同时改脚本,否则 summarize 输出(`thresholds` 字段会带脚本真值)与文档解释会漂移。后续提案可考虑把"脚本常量 ↔ 本表数字"双向校验补到 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh)。 - -## 9. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/validation_scenarios.md deleted file mode 100644 index 31d216a..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,161 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -### JSON 与本文同步流程 -按以下顺序执行,跳步会被对应脚本捕获: - -1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 -3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index dc83d7c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/12] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/12] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/12] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/12] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/12] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/12] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/12] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/12] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/12] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/12] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/12] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[12/12] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v59/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v6/metadata.json b/skills-engineering/ios-engineer/evolution/history/v6/metadata.json deleted file mode 100644 index 3ef1984..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v6", - "promoted_at": "2026-04-30T10:20:13+0800", - "source": "proposal:20260430-101809-downshift-current-architecture" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/SKILL.md deleted file mode 100644 index d4810b5..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/SKILL.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 -- 需要构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 首步分流 -先把任务归入一个主类,默认只读取该主类对应的 2 到 4 份文档;若命中高风险门禁再追加附加文档。当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 严格约束 UI 布局与可访问性,不用硬编码尺寸或魔法优先级修补设计问题。 -- 非必要场景不得使用 `priority(999)` 或同类技巧规避约束冲突。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v6/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v61/metadata.json b/skills-engineering/ios-engineer/evolution/history/v61/metadata.json deleted file mode 100644 index 739ac67..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v61", - "promoted_at": "2026-05-09T10:39:04+0800", - "source": "proposal:20260509-103358-ir-006-version-prerequisite-as-template-block" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/SKILL.md deleted file mode 100644 index 629185f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/SKILL.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,回答里必须出现一条显式的“版本前提”声明,二选一:要么给出从工程读取的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 真值(如 `iOS 15.0 / Swift 5.9`),要么以“假设 iOS ≥ N / Swift ≥ M,如不符请纠正”形式显式声明假设值。两者缺一或只给其中一项即视为违反本铁律。能读工程时优先读真值;只有在无法读取或成本过高时才允许退到显式假设。本 skill 不预设默认基线。具体落点见 [examples.md](references/examples.md) §1/§2/§4/§5/§6 模板的“版本前提”块与 [review_checklists.md](references/review_checklists.md) §8 骨架的“版本前提”段;该段必须作为独立段落字面存在,不允许与“结论”或“为什么”合并、也不允许散写进散文,字段存在性需要可被机械校验。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/anti_patterns.md deleted file mode 100644 index d5b8095..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,235 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 -- 本文件是**反模式库**(识别条件 / 风险 / 修法)。**审查检查表与可合入判定**归 [review_checklists.md](review_checklists.md);二者配合使用——审查时先按 review_checklists.md 维度过检,命中时回查本文件对应反模式条目。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_and_network.md deleted file mode 100644 index 17b0f90..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,120 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -本文件承担**实施类**架构设计与改造写法。**评估类**输出(架构体检 / 健康度评分 / 系统性风险排查 / 重构路线图)归 [architecture_analysis.md](architecture_analysis.md)。 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/code_templates.md deleted file mode 100644 index dbcc51e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/code_templates.md +++ /dev/null @@ -1,277 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 -- 使用本文件模板落地到产线的代码交付(PR 描述 / 合入说明 / 交付报告),必须附带一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许只写"已测试"或省略未覆盖项。与 [examples.md](examples.md) "残留风险声明"段字段对齐,保证四段式输出与产线代码交付两侧字段一致。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/examples.md deleted file mode 100644 index 1ef22ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/examples.md +++ /dev/null @@ -1,185 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 -- 涉及任何改动(排障修法 / 架构改动 / 并发迁移 / 性能优化 / 重构落地)的模板输出,"验证"段之后必须追加一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许把它们散写进"验证"段或合并成一段文字——字段存在性需要可被机械校验。 -- 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,必须在"结论"段之前追加一个独立的"版本前提"块,二选一:写出工程读取的真值(如 `iOS 15.0 / Swift 5.9`),或显式假设值(如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`)。该块必须作为独立段落字面存在,不允许与"结论"或"为什么"合并、也不允许散写进散文(履行 IR-006)。字段存在性需要可被机械校验。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 - -残留风险声明 -- 已覆盖:本次改动已经校验到的路径 / 场景 / 调用方 -- 未覆盖:明确没有验证到的路径 / 场景 / 调用方 -- 残留风险:即使上述都过了,仍可能出问题的假设 / 边界 / 依赖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 - -残留风险声明 -- 已覆盖:本次修复已经复现 / 回归验证到的路径 -- 未覆盖:没有验证到的路径 / 相近场景 / 相关调用方 -- 残留风险:根因假设若不成立会如何失败 / 还可能由哪些未知因素触发 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 - -残留风险声明 -- 已覆盖:本次并发改动已经验证过的调用点 / 线程边界 -- 未覆盖:未测试的异常路径 / 取消时机 / 并发度场景 -- 残留风险:Sendable 假设 / actor 重入 / 旧接口桥接里潜在的竞态 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 - -残留风险声明 -- 已覆盖:本次优化已经测过的指标 / 设备 / 场景 -- 未覆盖:没测到的设备档位 / 数据量级 / 交互路径 -- 残留风险:优化假设在哪些条件下会失效 / 是否可能拖累其他路径 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 - -残留风险声明 -- 已覆盖:已规划兼容层 / 已有回滚路径 / 已评估的阶段 -- 未覆盖:尚未做风险评估的子模块 / 未排期的阶段 -- 残留风险:阶段间耦合失败模式 / 发布窗口风险 / 观测盲区 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/review_checklists.md deleted file mode 100644 index 1fe8467..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/review_checklists.md +++ /dev/null @@ -1,103 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 - -残留风险声明 -- 已覆盖:本次审查过了哪些维度 / 改动路径 -- 未覆盖:没审到的路径 / 缺证据的维度(按 §1-§6 命中维度对账) -- 残留风险:即使合入后仍可能引发的回归 / 依赖其他团队确认的前提 -``` - -> 残留风险声明是 IR-008 在 findings-first 骨架里的落点:三字段必须作为独立子段字面存在,不得与"验证缺口"合并或省略。字段存在性会在回归场景里被机械校验。 -> 版本前提是 IR-006 在 findings-first 骨架里的落点:当审查涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义时必须作为独立段落字面存在,不得与"审查结论"合并;当审查完全不涉及上述维度时可省略,但需在"验证缺口"中显式声明"未涉及版本相关维度"。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/rule_index.md deleted file mode 100644 index 1b70b5c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/rule_index.md +++ /dev/null @@ -1,113 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,"结论"前必须有独立"版本前提"块(真值或显式假设),字段存在性可机械校验 | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言 | -| 残留风险声明(已覆盖 / 未覆盖 / 残留风险 三字段) | [SKILL.md](../SKILL.md) IR-008 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板末段;[review_checklists.md](review_checklists.md) §8 骨架末段;[code_templates.md](code_templates.md) 使用规则 | 改 owner 字段名或字段数必须同步三份引用文件对应段;三字段必须以独立段落字面存在,不得合并进"验证"段或"验证缺口"段;新增/缩减字段须先调整 owner 再批量同步所有引用位置 | -| 版本前提声明(iOS / Swift 真值或显式假设) | [SKILL.md](../SKILL.md) IR-006 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板首段;[review_checklists.md](review_checklists.md) §8 骨架首段;[validation_scenarios.md](validation_scenarios.md) 场景 3 通过标准 | 改 owner 字面(含二选一表述、触发维度集合)必须同步所有引用;新增模板必须同步插入"版本前提"块;该块作为独立段落字面存在不得合并入"结论"或"为什么"段;段标题"版本前提"是机械校验 anchor,重命名需批量同步全部引用位置 | -| 提案候选信号阈值 | [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72(4 个 `*_THRESHOLD` 常量) | [usage_ledger.md](usage_ledger.md) 第 8 节阈值表;[scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) `[11/13]` 步 | 改任一侧必须同步另一侧;validate_skill_evolution.sh `[11/13]` 步会自动断言不一致;新增第 5 个阈值需同步更新本表 + 文档 + 校验正则 | diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/testing_strategy.md deleted file mode 100644 index 29b22a2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,158 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件承担**测试规划**(层次划分 / 覆盖策略 / stub 设计)。**测试执行与失败修复**(跑测试 / 分析失败 / 平台验证排查)归 [test_execution_and_repair.md](test_execution_and_repair.md)。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/usage_ledger.md deleted file mode 100644 index 792af37..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,194 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 提案候选信号阈值 - -[scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 硬编码 4 个阈值常量,超过即在 summarize 输出中作为提案候选信号浮出。本节是这 4 个常量的文档化镜像: - -| 常量 | 值 | 候选提案信号 | 含义 | -|------|----|-------------|------| -| `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | -| `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | -| `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | - -**漂移防护**:阈值与 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 的 `*_THRESHOLD` 常量一一对应。改本文必须同时改脚本,否则 summarize 输出(`thresholds` 字段会带脚本真值)与文档解释会漂移。后续提案可考虑把"脚本常量 ↔ 本表数字"双向校验补到 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh)。 - -## 9. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/validation_scenarios.md deleted file mode 100644 index 2c99b19..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,166 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -### JSON 与本文同步流程 -按以下顺序执行,跳步会被对应脚本捕获: - -1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 -3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 -- 输出在“结论”段之前含独立的“版本前提”块(按 examples.md §4 模板),写出真值或显式假设(IR-006)。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 -- 给出并发 / 可用性 API / SwiftUI 行为建议但既无真值也无显式假设,隐性使用某个 iOS / Swift 版本的 API。 -- 未把版本前提作为独立块字面输出,仅在散文里隐含。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 -- 输出末尾含独立的“残留风险声明”块,固定三字段:已覆盖 / 未覆盖 / 残留风险,作为独立段落字面存在,不与“验证缺口”合并(IR-008)。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 -- 残留风险声明缺失、三字段不全、或被合并进“验证缺口”段。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 6ed9739..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/13] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/13] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/13] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/13] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/13] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/13] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/13] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/13] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/13] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/13] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/13] Validate threshold doc/script sync" -ruby <<'RUBY' -script_path = "scripts/summarize_usage_ledger.sh" -doc_path = "references/usage_ledger.md" - -script_consts = {} -File.foreach(script_path) do |line| - m = line.match(/^([A-Z_]+_THRESHOLD)\s*=\s*([0-9.]+)\s*$/) - next unless m - script_consts[m[1]] = m[2] -end - -required = %w[MISSED_RULE_THRESHOLD TASK_TYPE_OTHER_THRESHOLD DEVIATION_THRESHOLD TOOL_DIVERGENCE_THRESHOLD] -missing = required - script_consts.keys -unless missing.empty? - puts "Missing threshold constants in #{script_path}: #{missing.join(', ')}" - exit 1 -end - -doc_consts = {} -File.foreach(doc_path) do |line| - m = line.match(/^\|\s*`([A-Z_]+_THRESHOLD)`\s*\|\s*([0-9.]+)\s*\|/) - next unless m - doc_consts[m[1]] = m[2] -end - -doc_missing = required - doc_consts.keys -unless doc_missing.empty? - puts "Missing threshold rows in #{doc_path} §8: #{doc_missing.join(', ')}" - exit 1 -end - -drift = [] -required.each do |k| - if script_consts[k] != doc_consts[k] - drift << "#{k}: script=#{script_consts[k]} doc=#{doc_consts[k]}" - end -end -unless drift.empty? - puts "Threshold drift between #{script_path} and #{doc_path} §8:" - drift.each { |d| puts " #{d}" } - exit 1 -end -RUBY -echo "Threshold doc/script sync OK" - -echo "[12/13] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[13/13] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v61/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/metadata.json b/skills-engineering/ios-engineer/evolution/history/v62/metadata.json deleted file mode 100644 index 8f871d4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v62", - "promoted_at": "2026-05-09T10:46:10+0800", - "source": "proposal:20260509-104012-route-add-trigger-skip-anchors-per-route" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/SKILL.md deleted file mode 100644 index 2bd6aeb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/SKILL.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,回答里必须出现一条显式的“版本前提”声明,二选一:要么给出从工程读取的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 真值(如 `iOS 15.0 / Swift 5.9`),要么以“假设 iOS ≥ N / Swift ≥ M,如不符请纠正”形式显式声明假设值。两者缺一或只给其中一项即视为违反本铁律。能读工程时优先读真值;只有在无法读取或成本过高时才允许退到显式假设。本 skill 不预设默认基线。具体落点见 [examples.md](references/examples.md) §1/§2/§4/§5/§6 模板的“版本前提”块与 [review_checklists.md](references/review_checklists.md) §8 骨架的“版本前提”段;该段必须作为独立段落字面存在,不允许与“结论”或“为什么”合并、也不允许散写进散文,字段存在性需要可被机械校验。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 -- 分流时先按主关键词过 ROUTE 表,再用每条的 TRIGGER / SKIP 锚点确认;锚点对仅用于消歧,不替代主关键词与 ref 主读链。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:用户说「崩了 / 闪退 / EXC_BAD_ACCESS / 偶现 / 复现不出」;提供 crash log 堆栈;「线上某用户报告」。 - - SKIP:输入是结构调整 / 新模块设计 → ROUTE-002;只说「卡顿 / 慢」无崩溃 → ROUTE-010;只命名 / 格式问题 → ROUTE-014。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「怎么拆 / 怎么设计 / 状态归属 / 这个值从哪传」;新增模块 / 新页面前的设计;网络层重构。 - - SKIP:「项目越改越乱 / 健康度 / 路线图」→ ROUTE-003;已经在落地阶段 → ROUTE-012。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「项目体检 / 技术债 / 不敢动这块 / 重构从哪开始」;接手陌生项目;评估类需求。 - - SKIP:用户已有目标设计 / 拆分意图 → ROUTE-002;已进入迁移落地 → ROUTE-012。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 - - TRIGGER:「DTO / Entity / ViewState / ErrorModel / 怎么建模 / 字段映射」。 - - SKIP:仅 ViewState 流转 / 异步回写 → ROUTE-005;错误处理在网络层 → ROUTE-008。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「状态错乱 / 多 Bool 互斥 / 列表跳动 / 旧请求覆盖新 UI / 异步回写」。 - - SKIP:根因是任务取消 / actor / Sendable → ROUTE-007;是布局 / 约束冲突 → ROUTE-006。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 - - TRIGGER:「约束冲突 / 错位 / SwiftUI 抖动 / Auto Layout / 复用错乱 / 无障碍」。 - - SKIP:实质是状态错乱导致 UI 异常 → ROUTE-005;仅是性能(卡顿 / 掉帧)→ ROUTE-010。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「@MainActor / actor / Sendable / async let / 任务取消 / 数据竞争 / 死锁 / await 卡住」。 - - SKIP:仅状态归属 / UI 流转无并发竞态 → ROUTE-005;仅启动 / 列表性能热点 → ROUTE-010。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 - - TRIGGER:「请求失败 / 重试 / 鉴权刷新 / 401 / 分页 / 缓存 / 上传下载 / 幂等」。 - - SKIP:错误模型 / 分层定义 → ROUTE-004;取消语义 / Task 取消链 → ROUTE-007。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:「怎么记日志 / 日志规范 / 必记字段 / 排障取证 / 性能埋点 / 怎么观测」。 - - SKIP:日志只是手段、问题在崩溃定位 → ROUTE-001;性能量化指标本身 → ROUTE-010。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「启动慢 / 卡顿 / 滚动掉帧 / 内存上涨 / 过度刷新 / 能耗」。 - - SKIP:已确认是死锁 / await 阻塞 → ROUTE-007;仅 SwiftUI 重渲染但无指标证据 → ROUTE-006。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「review / 帮我看一下这个改动 / PR 看一下 / 这块代码」;提供 diff / patch / PR 链接。 - - SKIP:用户在描述自己的改动征求设计建议 → ROUTE-002;仅指出风格 / 命名问题 → ROUTE-014。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「灰度 / 回滚 / 阶段切 / UIKit 转 SwiftUI / callback 转 async/await / 兼容层」。 - - SKIP:还在评估阶段 / 路线图 → ROUTE-003;仅是设计 / 拆分 → ROUTE-002。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「Xcode build / Archive / IPA / TestFlight / CI / Fastlane / 发布观测」。 - - SKIP:编译错的根因是代码 / 类型问题 → ROUTE-014 或 ROUTE-001;性能数据收集 → ROUTE-009。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「命名规范 / 强制解包 / 访问控制 / 嵌套深 / 代码风格 / 术语」。 - - SKIP:是真实 bug 不只是风格 → ROUTE-001;是结构调整 / 拆分 → ROUTE-002。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「PR 拆分 / 多模块改 / ownership / 团队分工 / 谁该改这块」。 - - SKIP:是技术方案设计 → ROUTE-002;是审查具体 PR → ROUTE-011。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 - - TRIGGER:「搜索预算 / 子代理分流 / 多轮排查策略 / 日志取证预算」。 - - SKIP:具体排障 → ROUTE-001;具体性能分析 → ROUTE-010。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 - - TRIGGER:「接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构」;同时满足跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地任一升级判据。 - - SKIP:单点问题 / 单 ref 即可解决 → 走对应 ROUTE-001~016;仅"问题复杂"或"涉及多个 ref"不算升级条件。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - - TRIGGER:「skill / 规则缺失 / 规则冲突 / 验证场景 / 提案 / 自进化」;元工程 / SkillOps 维护任务。 - - SKIP:是业务问题答法 → 走 ROUTE-001~017。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/anti_patterns.md deleted file mode 100644 index d5b8095..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,235 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 -- 本文件是**反模式库**(识别条件 / 风险 / 修法)。**审查检查表与可合入判定**归 [review_checklists.md](review_checklists.md);二者配合使用——审查时先按 review_checklists.md 维度过检,命中时回查本文件对应反模式条目。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_and_network.md deleted file mode 100644 index 17b0f90..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,120 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -本文件承担**实施类**架构设计与改造写法。**评估类**输出(架构体检 / 健康度评分 / 系统性风险排查 / 重构路线图)归 [architecture_analysis.md](architecture_analysis.md)。 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/code_templates.md deleted file mode 100644 index dbcc51e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/code_templates.md +++ /dev/null @@ -1,277 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 -- 使用本文件模板落地到产线的代码交付(PR 描述 / 合入说明 / 交付报告),必须附带一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许只写"已测试"或省略未覆盖项。与 [examples.md](examples.md) "残留风险声明"段字段对齐,保证四段式输出与产线代码交付两侧字段一致。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/examples.md deleted file mode 100644 index 1ef22ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/examples.md +++ /dev/null @@ -1,185 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 -- 涉及任何改动(排障修法 / 架构改动 / 并发迁移 / 性能优化 / 重构落地)的模板输出,"验证"段之后必须追加一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许把它们散写进"验证"段或合并成一段文字——字段存在性需要可被机械校验。 -- 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,必须在"结论"段之前追加一个独立的"版本前提"块,二选一:写出工程读取的真值(如 `iOS 15.0 / Swift 5.9`),或显式假设值(如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`)。该块必须作为独立段落字面存在,不允许与"结论"或"为什么"合并、也不允许散写进散文(履行 IR-006)。字段存在性需要可被机械校验。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 - -残留风险声明 -- 已覆盖:本次改动已经校验到的路径 / 场景 / 调用方 -- 未覆盖:明确没有验证到的路径 / 场景 / 调用方 -- 残留风险:即使上述都过了,仍可能出问题的假设 / 边界 / 依赖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 - -残留风险声明 -- 已覆盖:本次修复已经复现 / 回归验证到的路径 -- 未覆盖:没有验证到的路径 / 相近场景 / 相关调用方 -- 残留风险:根因假设若不成立会如何失败 / 还可能由哪些未知因素触发 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 - -残留风险声明 -- 已覆盖:本次并发改动已经验证过的调用点 / 线程边界 -- 未覆盖:未测试的异常路径 / 取消时机 / 并发度场景 -- 残留风险:Sendable 假设 / actor 重入 / 旧接口桥接里潜在的竞态 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 - -残留风险声明 -- 已覆盖:本次优化已经测过的指标 / 设备 / 场景 -- 未覆盖:没测到的设备档位 / 数据量级 / 交互路径 -- 残留风险:优化假设在哪些条件下会失效 / 是否可能拖累其他路径 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 - -残留风险声明 -- 已覆盖:已规划兼容层 / 已有回滚路径 / 已评估的阶段 -- 未覆盖:尚未做风险评估的子模块 / 未排期的阶段 -- 残留风险:阶段间耦合失败模式 / 发布窗口风险 / 观测盲区 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/review_checklists.md deleted file mode 100644 index 1fe8467..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/review_checklists.md +++ /dev/null @@ -1,103 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 - -残留风险声明 -- 已覆盖:本次审查过了哪些维度 / 改动路径 -- 未覆盖:没审到的路径 / 缺证据的维度(按 §1-§6 命中维度对账) -- 残留风险:即使合入后仍可能引发的回归 / 依赖其他团队确认的前提 -``` - -> 残留风险声明是 IR-008 在 findings-first 骨架里的落点:三字段必须作为独立子段字面存在,不得与"验证缺口"合并或省略。字段存在性会在回归场景里被机械校验。 -> 版本前提是 IR-006 在 findings-first 骨架里的落点:当审查涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义时必须作为独立段落字面存在,不得与"审查结论"合并;当审查完全不涉及上述维度时可省略,但需在"验证缺口"中显式声明"未涉及版本相关维度"。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index d206d9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,117 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/rule_index.md deleted file mode 100644 index 68ce023..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/rule_index.md +++ /dev/null @@ -1,115 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先确认关键事实,不自行猜测 | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,"结论"前必须有独立"版本前提"块(真值或显式假设),字段存在性可机械校验 | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -每条 ROUTE 的 TRIGGER / SKIP 锚点对落在 SKILL.md 内对应 bullet 下方;本表"摘要"列只保留主关键词集,避免 SKILL.md 与本表双重维护 TRIGGER / SKIP。 - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言;新增 / 调整 ROUTE 的 TRIGGER / SKIP 锚点不改本表摘要列,只有主关键词集变化时才同步本表 | -| 残留风险声明(已覆盖 / 未覆盖 / 残留风险 三字段) | [SKILL.md](../SKILL.md) IR-008 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板末段;[review_checklists.md](review_checklists.md) §8 骨架末段;[code_templates.md](code_templates.md) 使用规则 | 改 owner 字段名或字段数必须同步三份引用文件对应段;三字段必须以独立段落字面存在,不得合并进"验证"段或"验证缺口"段;新增/缩减字段须先调整 owner 再批量同步所有引用位置 | -| 版本前提声明(iOS / Swift 真值或显式假设) | [SKILL.md](../SKILL.md) IR-006 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板首段;[review_checklists.md](review_checklists.md) §8 骨架首段;[validation_scenarios.md](validation_scenarios.md) 场景 3 通过标准 | 改 owner 字面(含二选一表述、触发维度集合)必须同步所有引用;新增模板必须同步插入"版本前提"块;该块作为独立段落字面存在不得合并入"结论"或"为什么"段;段标题"版本前提"是机械校验 anchor,重命名需批量同步全部引用位置 | -| 提案候选信号阈值 | [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72(4 个 `*_THRESHOLD` 常量) | [usage_ledger.md](usage_ledger.md) 第 8 节阈值表;[scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) `[11/13]` 步 | 改任一侧必须同步另一侧;validate_skill_evolution.sh `[11/13]` 步会自动断言不一致;新增第 5 个阈值需同步更新本表 + 文档 + 校验正则 | diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/testing_strategy.md deleted file mode 100644 index 29b22a2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,158 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件承担**测试规划**(层次划分 / 覆盖策略 / stub 设计)。**测试执行与失败修复**(跑测试 / 分析失败 / 平台验证排查)归 [test_execution_and_repair.md](test_execution_and_repair.md)。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/usage_ledger.md deleted file mode 100644 index 792af37..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,194 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 提案候选信号阈值 - -[scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 硬编码 4 个阈值常量,超过即在 summarize 输出中作为提案候选信号浮出。本节是这 4 个常量的文档化镜像: - -| 常量 | 值 | 候选提案信号 | 含义 | -|------|----|-------------|------| -| `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | -| `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | -| `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | - -**漂移防护**:阈值与 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 的 `*_THRESHOLD` 常量一一对应。改本文必须同时改脚本,否则 summarize 输出(`thresholds` 字段会带脚本真值)与文档解释会漂移。后续提案可考虑把"脚本常量 ↔ 本表数字"双向校验补到 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh)。 - -## 9. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/validation_scenarios.md deleted file mode 100644 index 2c99b19..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,166 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -### JSON 与本文同步流程 -按以下顺序执行,跳步会被对应脚本捕获: - -1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 -3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 -- 输出在“结论”段之前含独立的“版本前提”块(按 examples.md §4 模板),写出真值或显式假设(IR-006)。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 -- 给出并发 / 可用性 API / SwiftUI 行为建议但既无真值也无显式假设,隐性使用某个 iOS / Swift 版本的 API。 -- 未把版本前提作为独立块字面输出,仅在散文里隐含。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 -- 输出末尾含独立的“残留风险声明”块,固定三字段:已覆盖 / 未覆盖 / 残留风险,作为独立段落字面存在,不与“验证缺口”合并(IR-008)。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 -- 残留风险声明缺失、三字段不全、或被合并进“验证缺口”段。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 6ed9739..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/13] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/13] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/13] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/13] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/13] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/13] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/13] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/13] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/13] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/13] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/13] Validate threshold doc/script sync" -ruby <<'RUBY' -script_path = "scripts/summarize_usage_ledger.sh" -doc_path = "references/usage_ledger.md" - -script_consts = {} -File.foreach(script_path) do |line| - m = line.match(/^([A-Z_]+_THRESHOLD)\s*=\s*([0-9.]+)\s*$/) - next unless m - script_consts[m[1]] = m[2] -end - -required = %w[MISSED_RULE_THRESHOLD TASK_TYPE_OTHER_THRESHOLD DEVIATION_THRESHOLD TOOL_DIVERGENCE_THRESHOLD] -missing = required - script_consts.keys -unless missing.empty? - puts "Missing threshold constants in #{script_path}: #{missing.join(', ')}" - exit 1 -end - -doc_consts = {} -File.foreach(doc_path) do |line| - m = line.match(/^\|\s*`([A-Z_]+_THRESHOLD)`\s*\|\s*([0-9.]+)\s*\|/) - next unless m - doc_consts[m[1]] = m[2] -end - -doc_missing = required - doc_consts.keys -unless doc_missing.empty? - puts "Missing threshold rows in #{doc_path} §8: #{doc_missing.join(', ')}" - exit 1 -end - -drift = [] -required.each do |k| - if script_consts[k] != doc_consts[k] - drift << "#{k}: script=#{script_consts[k]} doc=#{doc_consts[k]}" - end -end -unless drift.empty? - puts "Threshold drift between #{script_path} and #{doc_path} §8:" - drift.each { |d| puts " #{d}" } - exit 1 -end -RUBY -echo "Threshold doc/script sync OK" - -echo "[12/13] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[13/13] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v62/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/metadata.json b/skills-engineering/ios-engineer/evolution/history/v63/metadata.json deleted file mode 100644 index 36e00cb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v63", - "promoted_at": "2026-05-09T10:51:30+0800", - "source": "proposal:20260509-104944-ir-002-clarification-block-as-template-trigger" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/SKILL.md deleted file mode 100644 index cacfeaa..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/SKILL.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 核心铁律 -- [IR-001] 始终使用简体中文。 -- [IR-002] 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。判定信息不足时(典型触发:模糊措辞 / 未给机型与系统 / 未给复现条件 / 未说已尝试方案 / 未说受影响范围),必须以独立的“前置确认”块字面输出 ≥1 个具体问题,方可继续给出方案。仅在散文中提“需要更多信息”或“建议补充”视为违反本铁律。前置确认问题维度示例见 [root_cause_enforcement.md](references/root_cause_enforcement.md) §2 取证策略;架构 / 性能类按对应 ROUTE 主读 ref 补完。能从工程或上下文读出的事实优先读,不要让用户重复输入。 -- [IR-003] 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- [IR-004] 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。**代码审查 / PR Review 例外**:按 findings-first 标准输出骨架输出,骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节。 -- [IR-005] 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,回答里必须出现一条显式的“版本前提”声明,二选一:要么给出从工程读取的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 真值(如 `iOS 15.0 / Swift 5.9`),要么以“假设 iOS ≥ N / Swift ≥ M,如不符请纠正”形式显式声明假设值。两者缺一或只给其中一项即视为违反本铁律。能读工程时优先读真值;只有在无法读取或成本过高时才允许退到显式假设。本 skill 不预设默认基线。具体落点见 [examples.md](references/examples.md) §1/§2/§4/§5/§6 模板的“版本前提”块与 [review_checklists.md](references/review_checklists.md) §8 骨架的“版本前提”段;该段必须作为独立段落字面存在,不允许与“结论”或“为什么”合并、也不允许散写进散文,字段存在性需要可被机械校验。 -- [IR-007] 不要格式化代码,除非明确要求格式化当前代码。 -- [IR-008] 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 - -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 - -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 -- 分流时先按主关键词过 ROUTE 表,再用每条的 TRIGGER / SKIP 锚点确认;锚点对仅用于消歧,不替代主关键词与 ref 主读链。 - -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 - -| 症状 / 关键词 | 优先入口 | 常见追加 | -|------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:用户说「崩了 / 闪退 / EXC_BAD_ACCESS / 偶现 / 复现不出」;提供 crash log 堆栈;「线上某用户报告」。 - - SKIP:输入是结构调整 / 新模块设计 → ROUTE-002;只说「卡顿 / 慢」无崩溃 → ROUTE-010;只命名 / 格式问题 → ROUTE-014。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「怎么拆 / 怎么设计 / 状态归属 / 这个值从哪传」;新增模块 / 新页面前的设计;网络层重构。 - - SKIP:「项目越改越乱 / 健康度 / 路线图」→ ROUTE-003;已经在落地阶段 → ROUTE-012。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「项目体检 / 技术债 / 不敢动这块 / 重构从哪开始」;接手陌生项目;评估类需求。 - - SKIP:用户已有目标设计 / 拆分意图 → ROUTE-002;已进入迁移落地 → ROUTE-012。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 - - TRIGGER:「DTO / Entity / ViewState / ErrorModel / 怎么建模 / 字段映射」。 - - SKIP:仅 ViewState 流转 / 异步回写 → ROUTE-005;错误处理在网络层 → ROUTE-008。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「状态错乱 / 多 Bool 互斥 / 列表跳动 / 旧请求覆盖新 UI / 异步回写」。 - - SKIP:根因是任务取消 / actor / Sendable → ROUTE-007;是布局 / 约束冲突 → ROUTE-006。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 - - TRIGGER:「约束冲突 / 错位 / SwiftUI 抖动 / Auto Layout / 复用错乱 / 无障碍」。 - - SKIP:实质是状态错乱导致 UI 异常 → ROUTE-005;仅是性能(卡顿 / 掉帧)→ ROUTE-010。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「@MainActor / actor / Sendable / async let / 任务取消 / 数据竞争 / 死锁 / await 卡住」。 - - SKIP:仅状态归属 / UI 流转无并发竞态 → ROUTE-005;仅启动 / 列表性能热点 → ROUTE-010。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 - - TRIGGER:「请求失败 / 重试 / 鉴权刷新 / 401 / 分页 / 缓存 / 上传下载 / 幂等」。 - - SKIP:错误模型 / 分层定义 → ROUTE-004;取消语义 / Task 取消链 → ROUTE-007。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:「怎么记日志 / 日志规范 / 必记字段 / 排障取证 / 性能埋点 / 怎么观测」。 - - SKIP:日志只是手段、问题在崩溃定位 → ROUTE-001;性能量化指标本身 → ROUTE-010。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「启动慢 / 卡顿 / 滚动掉帧 / 内存上涨 / 过度刷新 / 能耗」。 - - SKIP:已确认是死锁 / await 阻塞 → ROUTE-007;仅 SwiftUI 重渲染但无指标证据 → ROUTE-006。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「review / 帮我看一下这个改动 / PR 看一下 / 这块代码」;提供 diff / patch / PR 链接。 - - SKIP:用户在描述自己的改动征求设计建议 → ROUTE-002;仅指出风格 / 命名问题 → ROUTE-014。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「灰度 / 回滚 / 阶段切 / UIKit 转 SwiftUI / callback 转 async/await / 兼容层」。 - - SKIP:还在评估阶段 / 路线图 → ROUTE-003;仅是设计 / 拆分 → ROUTE-002。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「Xcode build / Archive / IPA / TestFlight / CI / Fastlane / 发布观测」。 - - SKIP:编译错的根因是代码 / 类型问题 → ROUTE-014 或 ROUTE-001;性能数据收集 → ROUTE-009。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「命名规范 / 强制解包 / 访问控制 / 嵌套深 / 代码风格 / 术语」。 - - SKIP:是真实 bug 不只是风格 → ROUTE-001;是结构调整 / 拆分 → ROUTE-002。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「PR 拆分 / 多模块改 / ownership / 团队分工 / 谁该改这块」。 - - SKIP:是技术方案设计 → ROUTE-002;是审查具体 PR → ROUTE-011。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算**:主读 [mcp_control.md](references/mcp_control.md)。 - - TRIGGER:「搜索预算 / 子代理分流 / 多轮排查策略 / 日志取证预算」。 - - SKIP:具体排障 → ROUTE-001;具体性能分析 → ROUTE-010。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 - - TRIGGER:「接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构」;同时满足跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地任一升级判据。 - - SKIP:单点问题 / 单 ref 即可解决 → 走对应 ROUTE-001~016;仅"问题复杂"或"涉及多个 ref"不算升级条件。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - - TRIGGER:「skill / 规则缺失 / 规则冲突 / 验证场景 / 提案 / 自进化」;元工程 / SkillOps 维护任务。 - - SKIP:是业务问题答法 → 走 ROUTE-001~017。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(触发条件见 IR-004 例外条款;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/anti_patterns.md deleted file mode 100644 index d5b8095..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,235 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 先按每条反模式的"识别条件"判定是否命中;未达到条件不贴标签。 -- 命中后按"表现 → 识别条件 → 风险 → 修法"四段输出;修法必须指向可验证的代码改动。 -- 本文件是**反模式库**(识别条件 / 风险 / 修法)。**审查检查表与可合入判定**归 [review_checklists.md](review_checklists.md);二者配合使用——审查时先按 review_checklists.md 维度过检,命中时回查本文件对应反模式条目。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -识别条件:同一类型同时承担 ≥ 3 类职责(例如渲染 + 网络 + 路由 + 埋点);或单类行数 > 600;或成员变量 > 20。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -识别条件:存在跨模块直接访问 internal / private 实现;或 SPM 包之间循环依赖;或模块 public API 占比 > 50%。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -识别条件:同一类型承担 ≥ 3 种不同职责(网络 + 缓存 + 业务 + 状态同步);或包含 ≥ 2 个需要锁保护的共享状态;或被 ≥ 10 个调用方持有为单例。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -识别条件:`Task {}` 出现在 UIView / Cell / 工具类;或该 Task 缺少对应的 cancel 触发链路;或 Task 修改共享状态但无归属对象(持有方不能回答"谁取消")。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -识别条件:新增 `main.async` 的 commit / PR 注释只写"修 crash / 白屏"而未解释为何原路径不在主线程;或连续多层 `main.async` 嵌套;或 async 后闭包捕获对象在非主线程已 dealloc 的证据。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -识别条件:添加 `@unchecked Sendable` 的位置无"内部同步保证"注释;或该类含可变 `var` 属性但无 lock / actor 保护;或该类跨多个任务并发写。 - -风险: -- 把真实数据竞争伪装成"已处理" - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -识别条件:同一语义状态(例如"已登录"、"正在加载"、"已选中")在 ≥ 2 个对象中独立维护;或 UI 层需要手动 "sync" 多处状态。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -识别条件:出现硬编码约束常量 ≥ 50 或字体大小 ≥ 13 的魔法值;或原本应由 `intrinsicContentSize` 决定的维度被硬写;或布局修复 commit 只改数字不改层级。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -识别条件:list item 的 id 使用 `indexPath` / 数组 index / 可变字段(如 `unreadCount` / `status` / `updatedAt`);或 item 更新时 identity 发生变化。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -识别条件:URL / Query / Header 使用 `+` 或 string interpolation 拼接 ≥ 3 处;或相同接口的 URL 拼装逻辑出现在 ≥ 2 个文件。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -识别条件:UI 代码直接展示 `error.localizedDescription` / `error.debugDescription`;或用户可见提示中出现 HTTP status code / NSError domain。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -识别条件:写操作(POST / PUT / DELETE)存在自动重试;或重试缺少 max attempts 或 backoff;或业务错误(4xx business fail)被纳入重试范围。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -识别条件:Time Profiler 显示主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿);或 `cellForItem` / `scrollViewDidScroll` / `layoutSubviews` 中执行 decode / JSON parse / sort 等 O(n) 以上操作。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取"更快"。 - -识别条件:使用缓存但未定义失效条件;或 `catch` 块吞异常无日志;或刷新代码被注释为"性能原因暂时跳过";或"避免重复请求"导致数据脏读。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -识别条件:修复 PR / commit 描述停留在"修了 xxx 崩溃"/"防御 xxx nil",未说明"为什么 xxx 会发生";或修复点是崩溃栈最后一帧而未回溯调用链。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -识别条件:修复代码只新增 `if` / `guard` / 空值检查 / `try-catch` 兜底,未删除或改变错误来源;或修复后相同输入路径仍可能触发相同错误。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_analysis.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_analysis.md deleted file mode 100644 index 9668a1c..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_analysis.md +++ /dev/null @@ -1,188 +0,0 @@ -# 架构分析与技术债盘点 - -## 适用场景 -用于以下任务: -- 对整个项目或某个业务域做**架构评审**、健康度评分、技术债等级判断 -- 做**系统性风险排查**:识别跨模块的稳定性、性能、可维护性隐患 -- 接手陌生代码库后,需要先沉淀索引、再给改造路线,而不是立刻动手修 -- 用户以"架构体检""当前架构有没有问题""技术债有多严重""系统性风险在哪"这类评估类问题发起咨询 - -本文件只定义**评估类输出**的纪律、字段和阶段。具体修复写法仍由各专项 ref 承担(架构 → [architecture_and_network.md](architecture_and_network.md)、并发 → [swift_concurrency.md](swift_concurrency.md)、性能 → [performance_optimization.md](performance_optimization.md) 等)。 - -## 为什么要这样约束 -"让 AI 稳定输出高质量架构分析"的真实难点不是分析能力,而是**防止四类劣化**: -1. 泛泛而谈:输出"建议解耦""建议加测试"这类无证据结论。 -2. 一次性铺开几十条:用户无法判断优先级,也无法落地。 -3. 最小修复和架构翻新混在一起:短期动作和长期动作挤在一条建议里。 -4. 越界推断:信息不足时仍然给结论,把猜测当事实输出。 - -本文件的每一条规则都针对其中一类劣化。若跳过任一条,输出质量会立刻退化,因此不允许精简执行。 - -## 使用规则 -- 进入 Phase 2 前,必须已完成 Phase 1 索引建立;没有索引不得输出风险等级、健康度评分或路线图。 -- 默认先执行 Phase 1 并停止;只有用户明确要求"继续完整分析"、"输出最终报告"或"一次性完成"时,才继续 Phase 2-4。 -- 每一轮输出**最多 5 条问题**,按严重级排序;多出来的降到下一轮或归为观察项。 -- 结论必须有代码证据;信息不足时以"待确认假设"明确标注,不得当作结论。 -- 先给"最小改动可落地方案 A",再给"长期最优方案 B";两者不得混写在同一段。 -- 遵守 SKILL.md 核心铁律(先锁定主路径 / 最小可验证修复优先 / 覆盖-未覆盖-残留风险)和 [root_cause_enforcement.md](root_cause_enforcement.md) 的根因纪律。 - -## 快捷用语 -当用户只说"架构体检"时,等价于: -- 对当前 iOS 项目执行本文件的架构分析剧本。 -- 先执行 Phase 1,只建立项目索引,不输出优化建议、健康度评分或风险等级。 -- Phase 1 必须覆盖模块职责、目录结构、核心业务链路、状态流 / 数据流、线程模型、网络层与缓存层。 -- 所有结论必须区分"已确认事实"和"待确认假设"。 -- 完成 Phase 1 后先停止,等待用户确认是否进入 Phase 2。 - -当用户说"完整架构体检"或"一次性架构体检"时,等价于: -- 完整执行 Phase 1-4 并输出最终报告。 -- 风险问题最多输出 Top 5,必须按严重级排序。 -- 每条风险必须包含本文件规定的 10 个必备字段。 -- 不输出无代码证据的泛泛结论。 -- 不修改代码,只做分析,除非用户明确要求修复。 - -## 角色与能力约束 -进入本剧本时,默认角色为项目的 Staff iOS Engineer,同时具备: -- 架构评审能力 -- 性能优化能力 -- 稳定性治理能力 -- 工程化与可维护性治理能力 - -目标:在不打断业务迭代的前提下,识别并排序**系统性风险**,输出可落地改造路线;不做重写式建议,不提与主风险无关的美化性重构。 - -## 分析范围 -固定按下列 6 个维度扫描;扫描顺序不等于输出顺序,输出以严重级排序。 - -### 1. 架构与模块 -- 模块边界是否清晰 -- 依赖方向是否合理(是否存在反向依赖 / 循环依赖) -- 分层是否稳定(UI / Domain / Data / Infra) -- 是否存在 Massive ViewController / God Object - -详细原则见 [architecture_and_network.md](architecture_and_network.md)。 - -### 2. 状态与数据流 -- 状态源是否唯一 -- 状态同步是否存在竞态 -- 数据流是否可追踪、可回放、可测试 -- 异步回调链是否导致状态漂移 - -详细模式见 [ui_state_patterns.md](ui_state_patterns.md)。 - -### 3. 并发与线程安全 -- `@MainActor` 使用是否正确 -- `async/await`、`Task` 生命周期是否安全 -- 是否存在 data race、死锁风险、优先级反转 -- 单例、缓存、共享可变状态是否线程安全 - -详细要求见 [swift_concurrency.md](swift_concurrency.md)。 - -### 4. 内存与生命周期 -- retain cycle、闭包捕获、Timer / Observer 是否正确释放 -- VC / ViewModel / Service 生命周期是否匹配 -- 图片与大对象管理是否合理(峰值内存风险) - -### 5. 性能与稳定性 -- 首屏、列表滚动、渲染阻塞 -- 离屏渲染、频繁布局、主线程重活 -- 网络重试、超时、取消、幂等、Token 刷新 -- 缓存一致性、脏读、击穿、雪崩 -- 崩溃高风险路径(空值、越界、并发时序) - -详细指标与路径见 [performance_optimization.md](performance_optimization.md) 与 [networking_patterns.md](networking_patterns.md)。 - -### 6. 工程化与可维护性 -- SOLID 违反点 -- 测试覆盖与可测试性(单测 / 集成测试) -- 可观测性(日志、埋点、错误分级) -- 重构阻力(耦合点、迁移成本) - -详细要求见 [testing_strategy.md](testing_strategy.md) 与 [observability_logging.md](observability_logging.md)。 - -## 每条问题的必备字段 -每条输出必须包含下列 10 个字段,缺一不可;若某字段无法给出,必须显式写"待确认"并说明缺什么信息。 - -1. **等级**:致命 / 高 / 中 / 低。判定标准: - - 致命:会直接导致 Crash、数据丢失、资损或大面积用户不可用 - - 高:稳定性 / 性能 / 安全性显著劣化,或核心业务迭代被结构性耦合持续拖慢 - - 中:可维护性或局部体验问题,长期累积会升级为高 - - 低:风格或一致性问题,不影响行为 -2. **位置**:文件路径 + 相关符号 / 方法(精确到类或函数) -3. **证据**:关键代码片段(尽量简短,保留能说明问题的上下文) -4. **问题机制**:为什么会发生(结构性原因,不只是现象描述) -5. **触发条件**:在什么场景下出现(设备、并发、网络、数据规模等) -6. **影响范围**:用户 / 业务 / 稳定性 / 性能中哪些被波及 -7. **修复方案 A — 最小改动**:低风险、可快速上线的止血方案 -8. **修复方案 B — 长期方案**:架构级优化方向 -9. **成本评估**:人天 + 关键风险点 -10. **收益评估**:稳定性 / 性能 / 维护性的可量化或可验证描述 - -缺字段是最常见的质量劣化来源。如果输出里看到"建议重构 XXX"但没有位置、证据、成本,该条必须打回重写,不得放行。 - -## 执行流程(严格按阶段,不得跳阶段) - -### Phase 1 — 项目索引建立(只理解,不优化) -目的:在给出任何结论之前,先沉淀可验证的事实底座。 - -优先读取: -1. 项目配置:`.xcodeproj` / `.xcworkspace` / `Package.swift` / `Podfile` -2. 目录与模块:源码目录、资源目录、测试目录、内部 framework / package -3. App 入口:`App` / `SceneDelegate` / `AppDelegate` / 根路由或根容器 -4. 组装层:依赖注入、Router / Coordinator、Service 注册、全局状态入口 -5. 数据边界:网络层、持久化、缓存、DTO / Entity / ViewState 映射 -6. 核心业务链路:启动、登录、首页、主要业务详情页或交易链路 -7. 质量入口:测试目录、CI 配置、日志与埋点封装 - -只输出: -1. 模块清单与职责 -2. 目录结构摘要 -3. 核心业务主链路 -4. 状态流 / 数据流路径 -5. 线程模型 -6. 网络层与缓存层结构 - -表达要求:明确区分"已确认事实"和"待确认假设",不得混写。**Phase 1 不得输出任何优化建议、打分或等级判断。** - -### Phase 2 — 架构与边界评估 -基于 Phase 1 的索引,只输出**致命 / 高**风险问题,最多 5 条;每条按"必备字段"10 条全部给出。 - -### Phase 3 — 并发 / 内存 / 性能深挖 -专项审查:主线程阻塞、列表渲染、异步时序、Task 生命周期、共享状态竞争、缓存一致性。 -最多 5 条,字段同 Phase 2。并发取证必须包含任务创建、写状态、切主线程这 3 条路径中的至少一条。 - -### Phase 4 — 分阶段重构路线图 -必须分成 3 段,每段独立给出目标、改动范围、风险、回滚策略、验收指标(可量化): -- 1–2 周快速止血 -- 1–2 月结构治理 -- 1–3 月架构升级 - -路线图与 Phase 2 / Phase 3 的问题必须**显式建立对应关系**(哪条问题由哪个阶段解决),不得给出无根问题的阶段动作。 - -## 最终输出格式 -完成 Phase 4 后,汇总按下列 8 节固定顺序输出;缺节需显式写"本轮不涉及",不得隐藏: - -1. **项目健康度评分(0–100,含评分依据)** -2. **架构成熟度与技术债等级** -3. **Top 风险清单**(最多 5 条,按严重级排序) -4. **立即行动项(1–2 周)** -5. **中期治理项(1–2 月)** -6. **长期演进建议(1–3 月)** -7. **重构路线图**(里程碑 / 依赖 / 验收标准) -8. **需补充信息**(若有;若无写"本轮信息充分") - -第 1 节评分必须列出扣分项与扣分依据,不得只给总分。第 8 节不是可选礼貌提示,而是输出纪律的一部分:任何被标为"待确认假设"的结论都必须在此节列出所需补充的信息。 - -## 反模式(会让分析失去可信度) -- 没有证据就下结论,或把"常见建议"当具体风险(如无证据地写"建议引入 Coordinator") -- 一次性输出超过 5 条风险,用户无法排序 -- 最小修复和长期方案混写,导致短期动作被架构翻新拖住 -- 路线图里出现没有对应问题的阶段动作 -- Phase 1 还没做就开始评分 -- 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 - -## 与其他 ref 的协作 -- 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) -- 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) -- 需要决策记录格式:[decision_records.md](decision_records.md) -- 需要审查维度清单:[review_checklists.md](review_checklists.md) -- 需要输出骨架的字段细节:[examples.md](examples.md) diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_and_network.md deleted file mode 100644 index 17b0f90..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,120 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -本文件承担**实施类**架构设计与改造写法。**评估类**输出(架构体检 / 健康度评分 / 系统性风险排查 / 重构路线图)归 [architecture_analysis.md](architecture_analysis.md)。 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路(完整链路单一定义,其他文件引用此处): - -```text -Endpoint -> RequestBuilder -> APIClient -> Decoder/DTO -> Repository/Mapper -> Entity -> UseCase -> ViewModel/ViewState -``` - -各环节职责: -- **Endpoint**:定义路径 / 方法 / Header / Body schema。 -- **RequestBuilder**:构造 `URLRequest`(或项目既有网络抽象的等价请求对象)。 -- **APIClient**:发送请求、接收响应、错误分层转换。 -- **Decoder/DTO**:把响应字节流解码为 DTO 数据传输对象(接口传输结构)。 -- **Repository/Mapper**:把 DTO 映射为 Entity 业务实体,聚合远端 / 缓存 / 持久化。 -- **Entity**:业务语义结构,脱离传输细节。 -- **UseCase**:业务用例编排(复杂业务场景必要,简单 CRUD 可省略)。 -- **ViewModel/ViewState**:界面状态编排和渲染结构。 - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 新建独立网络能力优先使用 `URLSession + async/await`(或项目已统一的等价抽象);既有网络层(例如自研 `NetworkManager`、Alamofire、Combine-based 抽象)按现有抽象扩展,不在局部改动中顺手迁移底层实现。底层迁移必须单独立项,参考 [migration_strategy.md](migration_strategy.md)。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误分层必须遵守 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"(6 层:传输 / 状态码 / 解码 / 鉴权 / 业务 / 展示),APIClient 层负责把前 3 层错误转为 ErrorModel。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -> 相关文件分工:链路职责 + 环节说明见本文件上方 "基础结构";网络模式细则(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重 / 常见反模式)见 [networking_patterns.md](networking_patterns.md);错误分层见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。本文件只保留网络层**架构边界**和跨层**安全规则**。 - -## 鉴权与安全 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 33de787..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,96 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -按错误特征识别失败层级: - -| 层级 | 典型错误信号 | 识别特征 | -| --- | --- | --- | -| 依赖解析 | `Package.resolved missing` / `version constraint unsolvable` / `pod install` 报 Podfile.lock 冲突 | 错误发生在构建开始前,提示文本包含 `version` / `resolved` / `dependency` | -| 编译 | `error: cannot find 'Foo' in scope` / `undeclared type` / Swift 类型不匹配 | 错误指向具体源文件与行号,提示含 `cannot find` / `undeclared` / `type mismatch` | -| 链接 | `Undefined symbol: _OBJC_CLASS_$_Foo` / `ld: framework not found` | 错误发生在编译通过后,提示含 `Undefined symbol` / `ld:` / `framework not found` | -| 签名 | `Code signing error` / `provisioning profile` / `entitlements` 问题 | 错误文本包含 `signing` / `provisioning` / `entitlement` / `team ID` | -| 打包 | 资源文件 missing / Info.plist 校验失败 / 归档失败 | 错误发生在链接后的归档阶段,提示含 `archive` / `Info.plist` / `resource` | -| 测试 | XCTest 断言失败 / 测试 target 配置错误 | 错误发生在测试 target 执行阶段,提示含 `XCTAssert` / `test failure` | - -判别流程:从上到下匹配错误信号;命中某层后先解决该层问题再继续构建,不跳跃处理下游。缓存清理或重新生成工程文件只在上述层级全部排除后使用。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/code_templates.md deleted file mode 100644 index dbcc51e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/code_templates.md +++ /dev/null @@ -1,277 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 -- 本文件内所有 `Feature*` 命名的类型(`FeatureEntity`、`FeatureRemoteDataSourceProtocol`、`FeatureCacheProtocol` 等)以及与具体业务解耦的协议占位(如 `LoggerProtocol`)均为**占位命名**,业务侧需替换为真实类型或定义对应协议;模板直接复制并不保证可编译。 -- 使用本文件模板落地到产线的代码交付(PR 描述 / 合入说明 / 交付报告),必须附带一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许只写"已测试"或省略未覆盖项。与 [examples.md](examples.md) "残留风险声明"段字段对齐,保证四段式输出与产线代码交付两侧字段一致。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - private let logger: LoggerProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol, - logger: LoggerProtocol - ) { - self.remote = remote - self.cache = cache - self.logger = logger - } - - func fetch() async throws -> FeatureEntity { - // 缓存读:区分"未命中 / 损坏 / 读失败",不用 try? 静默吞错 - do { - if let cached = try cache.read() { - return cached - } - } catch { - // 缓存读失败:必须记录;本模板选择降级到 remote - // 业务若不允许降级(例如离线首屏),改为 throw error - logger.error("cache read failed, falling back to remote: \(error)") - } - - let entity = try await remote.fetch() - - // 缓存写:失败必须记录,但成功路径已获得数据,不阻塞返回 - // 业务若要求强一致,改为 throw - do { - try cache.write(entity) - } catch { - logger.error("cache write failed: \(error)") - } - - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态:缓存读失败不得压成单一 nil 分支,必须显式记录并给出降级决策(降级 / throw);缓存写失败必须记录(哪怕不阻塞返回)。 -- `try?` 只适用于"失败即忽略、业务不关心原因"的场景;缓存路径不在此范围。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/decision_records.md deleted file mode 100644 index 830a71e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案新增、删除或移动公开 API(`public` / `package` 修饰符),或改变现有公开 API 的行为语义(返回值类型、异常集、副作用)。 -- 若方案引入新的并发隔离域(`actor` / `@MainActor` / 串行队列),或改变现有隔离策略(例如从 class + lock 改为 actor)。 -- 若方案移动或合并 ViewState / Entity / 共享状态的真实持有者(source of truth),或将原本由 A 类持有的状态改由 B 类持有。 -- 若方案要求其他团队的代码同步修改(跨 PR 依赖),或同一 release 内有 ≥ 2 个 Feature 包被改动。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/domain_modeling.md deleted file mode 100644 index b81e003..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,105 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误固定分为 6 层,按流经顺序: - 1. **传输错误**(网络不通、超时、DNS 失败) - 2. **状态码错误**(4xx / 5xx HTTP 响应) - 3. **解码错误**(JSON 不符 schema、必需字段缺失) - 4. **鉴权错误**(401 / 403 / token 过期) - 5. **业务错误**(服务端业务规则拒绝,例如 "余额不足") - 6. **展示错误**(面向用户的错误文案 + 可执行动作) -- 每层错误归属: - - 传输错误:APIClient / 项目既有网络抽象层捕获(URLSession / 自研 NetworkManager / Alamofire 等),转为 `ErrorModel.network`,不向上暴露 `NSError` 或底层 SDK 错误类型。 - - 状态码错误:APIClient 根据 code 映射(4xx → 客户端错误分支,5xx → 服务端错误分支)。 - - 解码错误:Decoder 层抛出,携带 schema 不匹配细节;不回退到展示层。 - - 鉴权错误:`AuthInterceptor` 统一处理(触发刷新 / 跳登录 / 降级只读)。 - - 业务错误:Repository / UseCase 层识别 `code + message`,不由 APIClient 判定业务语义。 - - 展示错误:ViewModel 把前 5 类错误映射为用户可见文案和动作(重试 / 返回 / 联系客服)。 -- 面向 UI 的 ErrorModel 必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性(可重试 / 可降级 / 终止)和用户动作。 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/examples.md deleted file mode 100644 index 1ef22ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/examples.md +++ /dev/null @@ -1,185 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 输出结构遵守 SKILL.md 核心铁律(四段式 + 单主路径 + 最小修复);本文件只提供每类场景的四段具体字段模板,不重复定义触发或候选策略。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 -- 涉及任何改动(排障修法 / 架构改动 / 并发迁移 / 性能优化 / 重构落地)的模板输出,"验证"段之后必须追加一个独立的"残留风险声明"块,固定三字段:已覆盖 / 未覆盖 / 残留风险(履行 IR-008)。三字段必须作为独立段落字面存在,不允许把它们散写进"验证"段或合并成一段文字——字段存在性需要可被机械校验。 -- 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,必须在"结论"段之前追加一个独立的"版本前提"块,二选一:写出工程读取的真值(如 `iOS 15.0 / Swift 5.9`),或显式假设值(如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`)。该块必须作为独立段落字面存在,不允许与"结论"或"为什么"合并、也不允许散写进散文(履行 IR-006)。字段存在性需要可被机械校验。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 - -残留风险声明 -- 已覆盖:本次改动已经校验到的路径 / 场景 / 调用方 -- 未覆盖:明确没有验证到的路径 / 场景 / 调用方 -- 残留风险:即使上述都过了,仍可能出问题的假设 / 边界 / 依赖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 - -残留风险声明 -- 已覆盖:本次修复已经复现 / 回归验证到的路径 -- 未覆盖:没有验证到的路径 / 相近场景 / 相关调用方 -- 残留风险:根因假设若不成立会如何失败 / 还可能由哪些未知因素触发 -``` - -## 3. 代码审查答法 -适用场景和输出结构(findings-first 骨架 + 命中维度过检)见 [review_checklists.md](review_checklists.md)。 -本文件不重复定义代码审查的输出骨架;审查输出格式、可合入判定、分维度检查项全部在 review_checklists.md 单一承担。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 - -残留风险声明 -- 已覆盖:本次并发改动已经验证过的调用点 / 线程边界 -- 未覆盖:未测试的异常路径 / 取消时机 / 并发度场景 -- 残留风险:Sendable 假设 / actor 重入 / 旧接口桥接里潜在的竞态 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 - -残留风险声明 -- 已覆盖:本次优化已经测过的指标 / 设备 / 场景 -- 未覆盖:没测到的设备档位 / 数据量级 / 交互路径 -- 残留风险:优化假设在哪些条件下会失效 / 是否可能拖累其他路径 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 - -残留风险声明 -- 已覆盖:已规划兼容层 / 已有回滚路径 / 已评估的阶段 -- 未覆盖:尚未做风险评估的子模块 / 未排期的阶段 -- 残留风险:阶段间耦合失败模式 / 发布窗口风险 / 观测盲区 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/execution_playbooks.md deleted file mode 100644 index bb35308..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,115 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 -- 任何剧本若涉及并发模型、可用性 API、SwiftUI 行为或迁移建议,进入步骤 1 前必须先确认 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION`;版本未知时不得给具体 API 选择或并发模式建议。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 反复偶现 Crash 系统排查 -- 性能专项 -- 并发架构迁移 -- 大型重构落地 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 反复偶现 Crash 系统排查 -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 性能专项 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 并发架构迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 大型重构落地 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ios_conventions.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ios_conventions.md deleted file mode 100644 index d6927fc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ios_conventions.md +++ /dev/null @@ -1,131 +0,0 @@ -# iOS 编码约定 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性、中文术语统一等编码习惯问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀编码习惯层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 本文件不预设 iOS / Swift 版本基线;并发写法、可用性 API、SwiftUI 行为类约束的具体取舍由实际工程的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 决定。版本敏感建议详见 SKILL.md 核心铁律。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## Swift 属性声明与位置 -- 能 `let` 则 `let`:属性默认不可变,不必要不暴露写入能力。 -- 需要延迟构造且初始化依赖运行时上下文(例如需要 `self` 的属性)时才用 `lazy var`;注意 `lazy var` 不是并发安全的,跨任务访问必须说明线程归属或改由 `actor` 持有。 -- `var` 属性必须最小化对外可见性:优先 `private(set)`;跨类可写 `var` 必须说明状态归属和写入路径。 -- 共享可变状态必须说明隔离策略(`actor` / `@MainActor` / 明确锁)。 -- 属性位置建议统一放在类结构末尾(初始化 / public API / private helpers 之后),避免不同访问级别的属性穿插分布。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让“当前作用域属性 vs 局部变量”在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明“前提是什么、失败代价是什么、为什么不能走错误路径”。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## Swift 命名 -- 变量与方法命名统一使用小驼峰,例如 `messageCount`、`refreshFeed()`。 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止把业务临时状态泛化命名为 `Snapshot` / `快照`(例如把"当前某视图的临时数据"命名为 `XxxSnapshot` 而不给业务语义),改用贴近业务的命名(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 -- **例外**:Apple API 自身的 Snapshot 类型(例如 `NSDiffableDataSourceSnapshot`、`UIViewControllerContextTransitioning.snapshotView`)保留原名不改写;测试框架的 snapshot testing 概念保留原名。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/mcp_control.md deleted file mode 100644 index 2b61bda..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/mcp_control.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 子代理分流 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -工具调用没有硬性总量;按以下可操作约束收敛: -- 只有在已经拿到新证据时才继续扩展调用。"新证据" 定义:上一次调用未见过的错误信息、新的日志行、新的代码文件、新的数据字段、或能证伪/证实当前假设的具体事实。仅"想到新的搜索词"不算新证据。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 子代理分流 -- 工作量较大、上下文占用高,且用户已明确允许使用子代理时,优先把独立的探索、审查或验证任务交给子代理,避免主上下文被大量日志、搜索结果、文件内容占满。 -- 只分流可独立闭环的任务,例如:批量文件巡检、跨 reference 重复规则扫描、测试失败日志归类、方案交叉审查;主代理保留根因判断、最终决策、代码整合和用户沟通。 -- 不把当前最阻塞的关键路径交给子代理;如果下一步必须依赖该结果,主代理应先本地完成或等子代理返回后再继续。 -- 给子代理的输入必须边界清楚:任务目标、允许读取范围、输出格式、不得修改的文件;涉及代码修改时必须明确文件所有权,避免并行冲突。 -- 子代理返回后,主代理必须复核其结论是否有证据支撑,并只把有效证据和结论带回主上下文。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。"失败" 定义:返回空结果、返回与上次完全相同的结果、命令执行非 0 退出、或结果与当前假设无关。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/migration_strategy.md deleted file mode 100644 index 67a3eac..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,135 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 迁移审查额外检查项 -做迁移相关 PR 审查时,除 [review_checklists.md](review_checklists.md) 的 6 维检查外,补充以下迁移专项检查: -- 是否按阶段拆分(建抽象 / 接兼容层 / 迁调用方 / 删旧实现 / 收口验证),而不是单次大变更? -- 是否有兼容层且定义了生命周期(何时删除、删除前置条件)? -- 是否明确灰度范围和回滚触发条件(Crash / 指标异常 / 业务失败率)? -- 是否验证了新旧链路行为一致性? -- 若涉及并发或状态模型迁移,是否专项验证取消、回写、隔离? - -审查输出格式:遵守 [review_checklists.md](review_checklists.md) 第 8 节的 findings-first 标准输出骨架;迁移相关的额外检查项按其严重级落入该骨架对应小节。 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/networking_patterns.md deleted file mode 100644 index 438f04b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,105 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -完整链路和各环节职责定义见 [architecture_and_network.md](architecture_and_network.md) "基础结构"。本文件聚焦具体网络模式(分页 / 重试 / 缓存 / 鉴权刷新 / 上传下载 / 幂等去重),不重复链路骨架。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -错误分层、每层归属、面向 UI 的映射规则,完整定义见 [domain_modeling.md](domain_modeling.md) "ErrorModel 建模规则"。 - -网络层(APIClient)职责:捕获传输错误 / 状态码错误 / 解码错误,转为 `ErrorModel` 后向上抛出;不直接把 `NSError` 或 HTTP code 暴露给 Repository 以上层。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/observability_logging.md deleted file mode 100644 index 6924b9b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/observability_logging.md +++ /dev/null @@ -1,97 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 当现有日志、指标、证据链不足以定位根因或验证修复时,先补齐**最小必要**可观测性(不是铺开完整观测体系);若证据已足够支撑最小修复,不应强制新增日志或埋点。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -### 性能取证工具(单一归属,其他文件引用此处) -- **Instruments**:苹果官方性能分析套件,下列工具为其模板实例。 -- **Time Profiler**:定位 CPU 和主线程热点;按调用栈聚合采样,适合找"哪个函数在主线程耗时最长"。 -- **Core Animation**:观察帧率、离屏渲染、混合层和光栅化压力;适合找"滚动卡顿是哪类渲染成本"。 -- **Allocations**:跟踪堆对象分配和释放;适合找"内存为什么涨"。 -- **Leaks**:自动检测内存泄漏;适合找"泄漏点具体在哪个对象"。 -- **Memory Graph**(Xcode Debug Navigator):可视化对象引用图;适合找"强引用环在哪里"。 -- **Points of Interest + OSLog**:代码中打信号点,在 Instruments 时间轴可见;适合标记关键链路耗时(例如 "首屏开始" → "首屏完成")。 -- **MetricKit**:线上采集崩溃、卡顿、能耗数据,次日 delivery;适合观察真实用户的性能趋势,不适合本地实时调试。 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/performance_optimization.md deleted file mode 100644 index 80abb3e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,69 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 按优先级处理:主线程单次调用耗时 > 16 ms(掉帧)或 > 100 ms(卡顿)→ 重复计算成本占总耗时 > 20% → SwiftUI `body` 重算频率 > 60Hz 或 UIKit `cellForItem` 调用时有同步 IO → 资源浪费(图片未缓存、对象未复用)。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. **先取证**:按 [observability_logging.md](observability_logging.md) "性能观测" 的指标口径 + 工具选择采集数据,明确当前指标值 + 触发路径。 -2. **对照阈值**:用上文"总原则"的阈值(> 16 ms 掉帧 / > 100 ms 卡顿 / 重复计算 > 20% / body 重算 > 60Hz)判定是否命中优化必要。 -3. **选主因**:定位到一个主因(主线程阻塞 / 过度刷新 / 重复计算 / 资源浪费 / 内存热点),按本文件下方对应专项(SwiftUI / UIKit / 启动 / 内存)做针对性优化。 -4. **前后对比**:用同一指标口径重新采集,确认指标下降且无行为回归。 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 工具选择 -性能取证工具(Instruments / Time Profiler / Core Animation / Allocations / Leaks / Memory Graph / Points of Interest / OSLog / MetricKit)的用途和采集方式见 [observability_logging.md](observability_logging.md) "性能观测"。本文件不重复维护工具清单。 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/review_checklists.md deleted file mode 100644 index 1fe8467..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/review_checklists.md +++ /dev/null @@ -1,103 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,先识别当前改动**命中**哪些维度(正确性 / 架构 / 并发 / 性能 / UI / 测试),再对命中维度按清单过检。未命中维度在审查结论中显式标注 "未涉及" 或 "无证据",不强行过检生成空泛内容。 -- 审查结论覆盖所有**命中**维度;未命中维度只作标注。判定"命中"的条件:该维度有真实代码改动或方案涉及;未改动的文件不视为命中。 -- 发现严重问题时,必须明确标记"不可合入"。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段 / 参数 / 状态是否已按 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 完成链路检查? -- [ ] 当前修复是否已列出已检查的影响面、未验证路径和残留风险?(不要求断言"无",要求显式标注) - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已回溯到真实拥有者 / 构造点 / 映射层?(详见 [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源") - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否给出了至少一种可复现验证路径,并显式列出未覆盖路径和对应的残留风险? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 修复 PR 没有列出已检查影响面 / 未验证路径 / 残留风险,且实际存在已知受影响模块未处理(缺交付证据,而不是断言无风险) - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 命中维度均过检;未命中维度已标注 未涉及 / 无证据 -- 无不可合入问题 -- 验证覆盖当前改动范围 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -版本前提 -- iOS / Swift 版本(工程真值,如 `iOS 15.0 / Swift 5.9`;或显式假设,如 `假设 iOS ≥ 15 / Swift ≥ 5.9,如不符请纠正`) - -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 - -残留风险声明 -- 已覆盖:本次审查过了哪些维度 / 改动路径 -- 未覆盖:没审到的路径 / 缺证据的维度(按 §1-§6 命中维度对账) -- 残留风险:即使合入后仍可能引发的回归 / 依赖其他团队确认的前提 -``` - -> 残留风险声明是 IR-008 在 findings-first 骨架里的落点:三字段必须作为独立子段字面存在,不得与"验证缺口"合并或省略。字段存在性会在回归场景里被机械校验。 -> 版本前提是 IR-006 在 findings-first 骨架里的落点:当审查涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义时必须作为独立段落字面存在,不得与"审查结论"合并;当审查完全不涉及上述维度时可省略,但需在"验证缺口"中显式声明"未涉及版本相关维度"。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 8e33136..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,127 +0,0 @@ -# 根因修复铁律 - -## 适用场景 -用于以下任务: -- 排障 / bug / 偶现问题 / Crash 的根因追查与修复评估 -- 代码审查、方案 Review 时判断改动是否只压症状、是否遗漏证据与影响面 -- 改动上线前确认已检查影响面、未验证路径与残留风险的显式声明 - -本文件只定义排障纪律、证据标准和伪修复禁令。通用输出模板归 SKILL.md 核心铁律,工具预算归 [mcp_control.md](mcp_control.md),本文件不重复定义。 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复时必须显式列出:已检查的影响面(哪些相关模块 / 状态 / 并发路径被看过)、未验证路径(哪些可能相关但没有复现或测试)、残留风险(如果某个未验证路径存在问题会发生什么)。不承诺"没有任何新风险"。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -前置确认问题维度(IR-002 落点;信息不足时以独立的"前置确认"块字面列出 ≥1 个): -- 机型:iPhone / iPad 型号(影响硬件性能档位 / 屏幕尺寸 / 内存档位 / Pro Motion)。 -- 系统版本:iOS / iPadOS 主版本号(影响可用 API、并发模型、SwiftUI 行为基线)。 -- 运行环境:真机 vs 模拟器 / Debug vs Release / 是否 TestFlight。 -- 复现条件:每次必现 / 偶现 / 特定路径触发;最小复现步骤;首次出现的版本 / 时间。 -- 已尝试方案:用户已经验证过 / 排除过的修法(避免重复无效路径)。 -- 受影响范围:单用户 / 部分用户 / 全部用户;线上 vs dev;是否有用户上报或监控数据。 - -按"区分主假设所必需"原则只问最少必要项;不要把六条全部抛给用户。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题(iOS 排障唯一专项,不在 anti_patterns.md 单独列出): -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -更广泛的排障反模式(现象即根因、补丁式修复:新增兜底 if、延迟、兜底分支、重试碰运气、DispatchQueue.main.async 掩盖时序)参考 [anti_patterns.md](anti_patterns.md) 第 6 节"排障反模式"。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/rule_index.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/rule_index.md deleted file mode 100644 index 5fc4a18..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/rule_index.md +++ /dev/null @@ -1,116 +0,0 @@ -# 规则 ID 索引 - -## 使用规则 -- 本文件是 [SKILL.md](../SKILL.md) 内 rule-ID 的真值索引。新增 / 修改 / 退役 ID **先改本文,再同步 SKILL.md**。 -- 自动校验脚本 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 断言两侧 ID 集合双向一致;不一致即非零退出。 -- ID 格式:`^[A-Z]+-\d{3}$`,前缀分四类: - - `IR-NNN` — 核心铁律(Iron Rule),全局生效 - - `SYM-NNN` — 症状导航表行(Symptom routing row) - - `ROUTE-NNN` — 任务分流 bullet(Task routing entry) - - `OUT-NNN` — 输出模板条目(Output template entry) -- ID 一旦发布不复用:退役后保留在「退役记录」节,标 `retired`,并指明替代 ID(无替代标 `retired-no-replacement`)。退役 ID 在 SKILL.md 中**不应再出现**——校验脚本会报警。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种);语义靠本表的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- 编号可有空洞(如 `IR-002` 之后跳到 `IR-007`),无强制连续约束;新增条目优先使用前缀内最大编号 +1。 - -## 铁律 IR-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文 | `## 核心铁律` | -| IR-002 | active | 描述不清 / 上下文不足 / 歧义时先以独立"前置确认"块字面输出 ≥1 个具体问题,不允许仅在散文里说"需要更多信息" | 同上 | -| IR-003 | active | 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选 | 同上 | -| IR-004 | active | 默认按「根因 → 为什么 → 修法 → 验证」四段式输出;review 例外走 findings-first | 同上 | -| IR-005 | active | 先给最小可验证修复,不先提出整模块重写或大范围重构 | 同上 | -| IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,"结论"前必须有独立"版本前提"块(真值或显式假设),字段存在性可机械校验 | 同上 | -| IR-007 | active | 不要格式化代码,除非明确要求 | 同上 | -| IR-008 | active | 任何改动都必须声明「已覆盖、未覆盖、残留风险」 | 同上 | - -## 症状导航 SYM-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| SYM-001 | active | Crash / 崩溃 / 断言 / 强解 / 野指针 → root_cause_enforcement.md | `### 症状导航` | -| SYM-002 | active | UI 错位 / 约束冲突 / 列表跳动 / 无障碍 → layout_and_ui.md | 同上 | -| SYM-003 | active | 状态错乱 / 异步回写 / 旧请求覆盖 → ui_state_patterns.md | 同上 | -| SYM-004 | active | 请求失败 / 鉴权刷新 / 分页或缓存问题 → networking_patterns.md | 同上 | -| SYM-005 | active | 卡顿 / 启动慢 / 内存上涨 / 能耗 → performance_optimization.md | 同上 | -| SYM-006 | active | 命名混乱 / 强制解包 / 访问控制 → ios_conventions.md | 同上 | -| SYM-007 | active | 老项目越改越乱 / 不敢动某块 / 接手陌生项目无入口 → architecture_analysis.md | 同上 | - -## 任务分流 ROUTE-NNN - -每条 ROUTE 的 TRIGGER / SKIP 锚点对落在 SKILL.md 内对应 bullet 下方;本表"摘要"列只保留主关键词集,避免 SKILL.md 与本表双重维护 TRIGGER / SKIP。 - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| ROUTE-001 | active | 排障 / Bug / 偶现问题 / Crash → root_cause_enforcement.md | `## 任务分流` | -| ROUTE-002 | active | 架构设计 / 模块拆分 / 状态归属 / 参数透传 → architecture_and_network.md | 同上 | -| ROUTE-003 | active | 架构分析 / 项目健康度 / 重构路线图 → architecture_analysis.md | 同上 | -| ROUTE-004 | active | 数据建模 / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | 同上 | -| ROUTE-005 | active | UI 状态 / 列表 / 表单 / 异步回写 → ui_state_patterns.md | 同上 | -| ROUTE-006 | active | UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 → layout_and_ui.md | 同上 | -| ROUTE-007 | active | 并发 / 取消链路 / actor / Sendable → swift_concurrency.md | 同上 | -| ROUTE-008 | active | 网络模式 / 分页 / 缓存 / 重试 / 鉴权 → networking_patterns.md | 同上 | -| ROUTE-009 | active | 日志 / 可观测性 / 必记字段 / 排障取证 → observability_logging.md | 同上 | -| ROUTE-010 | active | 性能 / 启动 / 列表卡顿 / 内存 / 能耗 → performance_optimization.md | 同上 | -| ROUTE-011 | active | 代码审查 / PR Review / 方案 Review → review_checklists.md | 同上 | -| ROUTE-012 | active | 重构落地 / 迁移 / 灰度 / 回滚 → migration_strategy.md | 同上 | -| ROUTE-013 | active | 构建 / CI / 发布观测 → build_release_and_ci.md | 同上 | -| ROUTE-014 | active | 编码约定 / 术语 / 命名 / 访问控制 → ios_conventions.md | 同上 | -| ROUTE-015 | active | 跨模块协作 / ownership / PR 拆分 / 技术债 → team_collaboration.md | 同上 | -| ROUTE-016 | active | 工具预算 / 子代理分流 / 多轮排查 / 搜索控制 → mcp_control.md | 同上 | -| ROUTE-017 | active | 复杂任务剧本(升级判据见 SKILL.md `### 路由优先级`)→ execution_playbooks.md | 同上 | -| ROUTE-018 | active | Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景 → self_evolution.md | 同上 | - -## 输出模板 OUT-NNN - -| ID | Status | 摘要 | SKILL.md 锚点 | -|----|--------|------|---------------| -| OUT-001 | active | 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板 → examples.md | `## 输出模板` | -| OUT-002 | active | 代码审查 / PR Review:findings-first 骨架(触发条件见 IR-004)→ review_checklists.md 第 8 节 | 同上 | -| OUT-003 | active | 产线代码骨架 → code_templates.md | 同上 | -| OUT-004 | active | 测试策略 / 验证范围 → testing_strategy.md | 同上 | -| OUT-005 | active | 架构裁决记录 → decision_records.md | 同上 | -| OUT-006 | active | iOS 测试体系建设 / 执行测试并修复失败 → test_execution_and_repair.md + testing_strategy.md | 同上 | - -## OUT 子单元映射 - -`OUT-NNN` 编号映射到的 ref 文件常包含多个独立子单元(模板章节、剧本章节、双文件分工)。本表是反向定位辅助,不替代 OUT-NNN ID 治理;新增模板 / 剧本时同步更新本表。 - -| OUT-ID | 子单元名 | 文件锚点 | 适用场景 | -|--------|----------|----------|----------| -| OUT-003 | ViewModel 模板 | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI 状态驱动页面 / 列表表单详情页状态编排 | -| OUT-003 | UseCase 模板 | [code_templates.md](code_templates.md) "## UseCase 模板" | 业务规则聚合 / 多数据源编排 / 领域层输入输出建模 | -| OUT-003 | Repository 模板 | [code_templates.md](code_templates.md) "## Repository 模板" | 远端 + 本地缓存聚合 / 解耦 Service 与业务层 | -| OUT-003 | APIClient 模板 | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / 强类型错误建模 | -| OUT-003 | Coordinator 模板 | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit 导航编排 / Feature 路由解耦 | -| OUT-003 | Actor 模板 | [code_templates.md](code_templates.md) "## Actor 模板" | 共享可变状态隔离 / Token 刷新 / 内存缓存 / 请求去重 | -| OUT-006 | 测试规划(分层与覆盖策略) | [testing_strategy.md](testing_strategy.md) | 设计测试时按层选 stub / 决定覆盖范围 | -| OUT-006 | 测试执行与失败修复 | [test_execution_and_repair.md](test_execution_and_repair.md) | 跑测试 / 分析失败 / 决定补还是修 | -| ROUTE-017 | 接手遗留页面 | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | 超大 ViewController / 状态散落 / UIKit + SwiftUI 混合老页面 | -| ROUTE-017 | 反复偶现 Crash 系统排查 | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | 难复现崩溃 / 线上偶发异常 / 随机状态错乱 | -| ROUTE-017 | 性能专项 | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | 启动慢 / 列表卡顿 / 页面刷新重 / 内存异常增长 | -| ROUTE-017 | 并发架构迁移 | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback 迁 async/await / GCD 迁结构化并发 / 串行队列迁 actor | -| ROUTE-017 | 大型重构落地 | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | 模块拆分 / 导航重建 / 状态模型重建 / 网络层重构 | - -## 退役记录 - -| ID | Status | 退役原因 | 替代 ID | 退役提案 | -|----|--------|----------|---------|----------| -| ROUTE-019 | retired | 与 ROUTE-018 真重复:ROUTE-019 把"Skill 验证场景"路由到 validation_scenarios.md,而 ROUTE-018 已声明"需要验证场景追加 validation_scenarios.md"。退役后"Skill 验证场景"关键词并入 ROUTE-018 主关键词集。 | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | -| IR-009 | retired | 是 9 条 IR 里唯一把执行委托给 ref 的 meta-IR("统一遵守 ios_conventions.md"),与其它 8 条具体行为指令不同层;其职能已被 ROUTE-014("编码约定 → ios_conventions.md")覆盖。退役后 IR 层仅保留具体行为指令,表达一致。 | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | - -## 跨文件共享概念索引 - -兑现 [self_evolution.md](self_evolution.md) "候选版约束" 中"涉及跨文件共享概念的提案必须 grep 全量位置覆盖"的执行细则。修改 owner 位置时必须同步遍历所有引用位置;改引用位置不动 owner 视为局部澄清,不进入跨文件提案范围。 - -| 概念 | Owner 位置 | 引用位置 | 修改协议 | -|------|-----------|---------|----------| -| 四段式输出(根因 → 为什么 → 修法 → 验证) | [SKILL.md](../SKILL.md) IR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6;[decision_records.md](decision_records.md) L5;[test_execution_and_repair.md](test_execution_and_repair.md) L82;[validation_scenarios.md](validation_scenarios.md) L26 / L88;[migration_strategy.md](migration_strategy.md)(剧本产物层) | 改 owner 必须同步所有引用;任一引用句式偏离 owner 即视为漂移 | -| findings-first 骨架(review 输出) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) IR-004 例外条款;[SKILL.md](../SKILL.md) OUT-002;[examples.md](examples.md) §3;[migration_strategy.md](migration_strategy.md) L114 | 改 owner 骨架字段必须同步 SKILL.md IR-004 / OUT-002 描述与 examples.md §3 引用句 | -| 参数透传与数据来源 | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" 节 | [SKILL.md](../SKILL.md) ROUTE-002;[review_checklists.md](review_checklists.md) §1 / §2;[validation_scenarios.md](validation_scenarios.md) 场景 2 | 改 owner 节标题必须同步 review_checklists.md 内对该节的字面引用;改概念定义必须同步 SKILL.md ROUTE-002 关键词 | -| 任务分流主关键词集 | [SKILL.md](../SKILL.md) ROUTE 表 | 本文件 ROUTE-NNN 摘要列;[mcp_control.md](mcp_control.md)(按工具预算分流) | 改 SKILL.md ROUTE 关键词必须同步本文件摘要列;新增 ROUTE 必须同步 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言;新增 / 调整 ROUTE 的 TRIGGER / SKIP 锚点不改本表摘要列,只有主关键词集变化时才同步本表 | -| 残留风险声明(已覆盖 / 未覆盖 / 残留风险 三字段) | [SKILL.md](../SKILL.md) IR-008 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板末段;[review_checklists.md](review_checklists.md) §8 骨架末段;[code_templates.md](code_templates.md) 使用规则 | 改 owner 字段名或字段数必须同步三份引用文件对应段;三字段必须以独立段落字面存在,不得合并进"验证"段或"验证缺口"段;新增/缩减字段须先调整 owner 再批量同步所有引用位置 | -| 版本前提声明(iOS / Swift 真值或显式假设) | [SKILL.md](../SKILL.md) IR-006 | [examples.md](examples.md) 使用规则 + §1/§2/§4/§5/§6 模板首段;[review_checklists.md](review_checklists.md) §8 骨架首段;[validation_scenarios.md](validation_scenarios.md) 场景 3 通过标准 | 改 owner 字面(含二选一表述、触发维度集合)必须同步所有引用;新增模板必须同步插入"版本前提"块;该块作为独立段落字面存在不得合并入"结论"或"为什么"段;段标题"版本前提"是机械校验 anchor,重命名需批量同步全部引用位置 | -| 前置确认块(IR-002 在信息不足时的字面化触发) | [SKILL.md](../SKILL.md) IR-002 | [root_cause_enforcement.md](root_cause_enforcement.md) §2 取证策略"前置确认问题维度"小节 | 改 owner 字面(如触发条件枚举)必须同步 root_cause_enforcement.md 维度示例;新增追问维度示例由对应 ROUTE 主读 ref 承担,不写进 owner,避免 SKILL.md 维度膨胀;段标题"前置确认"是机械校验 anchor,重命名需批量同步全部引用位置 | -| 提案候选信号阈值 | [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72(4 个 `*_THRESHOLD` 常量) | [usage_ledger.md](usage_ledger.md) 第 8 节阈值表;[scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) `[11/13]` 步 | 改任一侧必须同步另一侧;validate_skill_evolution.sh `[11/13]` 步会自动断言不一致;新增第 5 个阈值需同步更新本表 + 文档 + 校验正则 | diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/self_evolution.md deleted file mode 100644 index 95769dc..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/self_evolution.md +++ /dev/null @@ -1,145 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 规则 ID 治理 -- 真实任务观测 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。场景规格沉淀在 [evolution/scenarios/](../evolution/scenarios/),写入的 `scenario` 字段必须落在那 6 个固定 slug 内,否则后续 grader 无法对账。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 涉及跨文件共享概念(链路 / 分层 / 输出格式 / 分流表 / 术语条目等多文件引用的概念)的提案,生成候选版前必须先在 SKILL.md + references/ 全量 grep 该概念,列出所有出现位置,并在提案"变更内容"中覆盖所有位置(或显式标注为后续提案范围);不得只改单一位置就认为修正完成。常见跨文件共享概念举例:网络链路 / 错误分层 / 状态分层 / 建模分层 / 日志分层 / 四段式输出(owner: SKILL.md 核心铁律)/ findings-first 骨架(owner: review_checklists.md 第 8 节)/ 任务分流 / 术语定义。 -- 提案中使用"见 X 文件某节"这类跨文件引用时,必须先打开 X 文件该节确认实际包含被引用的内容;不得引用"未来意图承担但当前缺失"的内容。若引用的内容在目标文件尚不存在,要么同时在本提案中补齐目标文件内容,要么在提案"变更内容"中显式标注"需配合另一提案补齐目标文件 X 的某节",不得单独提交。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 规则 ID 治理 -- SKILL.md 中所有结构化规则都带 `[ID]` 前缀(铁律 IR-NNN / 症状导航 SYM-NNN / 任务分流 ROUTE-NNN / 输出模板 OUT-NNN);ID 真值索引沉淀在 [rule_index.md](rule_index.md)。 -- 新增 ID **先改 [rule_index.md](rule_index.md),再同步 SKILL.md**;两侧由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 双向断言一致。 -- ID 一旦发布不复用:退役时把 [rule_index.md](rule_index.md) 中的 status 改为 `retired` 或 `deprecated` 并填替代 ID(无替代填 `retired-no-replacement`),同时**从 SKILL.md 中删除 inline 引用**——校验脚本会拒绝退役 ID 仍出现在 SKILL.md 的情况。 -- 编号可有空洞,无强制连续约束;新增条目优先使用前缀内最大编号 +1。 -- ID 不携带语义后缀(不写 `ROUTE-LAYOUT-001` 这种),语义靠 [rule_index.md](rule_index.md) 的「摘要」列传达,避免重命名/拆分时出现 ID 含义漂移。 -- [evolution/scenarios/*.json](../evolution/scenarios/) 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` 字段可填 SKILL.md 中已存在的 active ID,用于跨场景统计命中频率;填 retired/deprecated ID 或不存在的 ID 时校验脚本会失败。 - -## 真实任务观测 -- 真实任务命中数据沉淀在 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl),schema、写入协议、三端 audit 块格式与 Codex / Claude Code / Cursor 各自的 system-prompt 片段统一沉淀在 [usage_ledger.md](usage_ledger.md)。 -- 写入路径有两条:单条用 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh)。两条路径都会原子拒绝非法条目,不污染 ledger。 -- ledger 的合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守,集成在伞形校验的 `[8/12]` 步:rule_id 必须在 [rule_index.md](rule_index.md) active 集合内,`task_type` 必须在 6 个固定场景 slug + `other` 之内,`missed_rules == expected_rules - hit_rules`。 -- ledger 是后续 summarize / 提案聚类(Step 4)的数据源。三端 audit 块由 LLM 自评,存在 self-grading 偏差——data 应被视作**有偏的草稿**,真正可信的命中率仍要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 -- 不要只记败例:平稳成功的任务也要追加,否则采样偏差会让命中率统计失真。 -- 定期跑 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 看汇总报表与提案候选信号(高频 missed_rules / `task_type=other` 累积 / 重复 deviation / 工具间 hit_rate 差异);脚本只读不写仓库,默认输出 markdown 到 stdout,`--json` 输出机器可读,`--since` / `--tool` 缩窄数据集。阈值硬编码在脚本顶部(missed≥3 / other≥5 / dev≥2 / 工具差≥40%)。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 -- 改动跨文件共享概念时,只改一处就提交候选版,不 grep 其他引用位置。 -- 使用跨文件引用("见 X 文件"、"详见 Y"、"按 Z 执行")时,未验证目标文件实际包含被引用内容就提交候选版(dead reference)。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/swift_concurrency.md deleted file mode 100644 index f284dc2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,62 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -以下并发专项信号(anti_patterns.md 第 2 节未覆盖,属于并发隔离/竞争/过期回写专项): -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI - -更广泛的并发反模式(散落式 `Task {}`、`DispatchQueue.main.async` 掩盖时序、滥用 `@unchecked Sendable`)参考 [anti_patterns.md](anti_patterns.md) 第 2 节"并发反模式"。 - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/test_execution_and_repair.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/test_execution_and_repair.md deleted file mode 100644 index 51abb42..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/test_execution_and_repair.md +++ /dev/null @@ -1,100 +0,0 @@ -# 测试执行与失败修复 - -## 适用场景 -用于以下任务: -- 构建 iOS 测试体系、补全核心业务测试 -- 执行测试并在失败暴露缺陷后进行最小可验证修复 -- 处理 iOS 专有平台验证场景(UIKit / iOS-only framework / Simulator UDID 选择等)导致的 `swift test` 误用排查 - -目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。本文件不承担测试层次划分与测试场景模板设计,那归 [testing_strategy.md](testing_strategy.md)。 - -## 项目背景 -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -## 验证命令 -- 对包含 `UIKit` / iOS-only API / 仅面向 iOS 的 framework 的 SPM 包,不要用裸 `swift test` 做最终验证;它默认按当前主机平台构建,常见失败是 `no such module 'UIKit'`。这种失败通常表示验证命令目标平台错了,不等价于源码在 iOS 下不可编译。 -- 先查 workspace / project 的 scheme 与可用模拟器: - - ```sh - xcodebuild -list -workspace - xcodebuild -showdestinations -workspace -scheme - ``` - - 只有 `.xcodeproj` 时,把 `-workspace ` 替换为 `-project `。 - -- 用 iOS Simulator SDK 构建包或 app scheme: - - ```sh - xcodebuild build \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 用同一个模拟器执行测试: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,name=,OS=' - ``` - -- 若存在多个同名 destination,优先使用 `-showdestinations` 输出中的 `id` 精确指定: - - ```sh - xcodebuild test \ - -workspace \ - -scheme \ - -destination 'platform=iOS Simulator,id=' - ``` - -## 核心要求 -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并按四段式(根因 / 为什么 / 修法 / 验证)输出结论。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -## 工作原则 -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/testing_strategy.md deleted file mode 100644 index 29b22a2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,158 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件承担**测试规划**(层次划分 / 覆盖策略 / stub 设计)。**测试执行与失败修复**(跑测试 / 分析失败 / 平台验证排查)归 [test_execution_and_repair.md](test_execution_and_repair.md)。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须给出至少一种可复现验证路径。 - -> "已覆盖 / 未覆盖 / 残留风险" 声明由 SKILL.md 核心铁律统一要求,本文件不重复。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ui_state_patterns.md deleted file mode 100644 index fe18688..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,121 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -> 本文 "状态分层" 是**运行时语义**分层(领域 / 页面 / 组件),定义某个状态属于哪个语义层级; -> [domain_modeling.md](domain_modeling.md) "建模分层"(DTO / Entity / ViewState / ErrorModel)是**数据类型结构**分层,定义某个数据在代码层的类型归属。 -> 两者正交:例如"正在加载"这个语义状态,既属于页面状态层,又用 ViewState 类型表达。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/usage_ledger.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/usage_ledger.md deleted file mode 100644 index 792af37..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/usage_ledger.md +++ /dev/null @@ -1,194 +0,0 @@ -# Usage Ledger(真实任务命中观测) - -## 用途 -- 把每次真实 iOS 工程任务结束后的「期望命中 / 实际命中 / 偏差 / 结果」结构化追加到 [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl)。 -- 是 Step 4 summarize / 提案聚类的数据源;本文件只定义 schema 与写入协议,**不实现统计**。 -- 维护人/工具:写入靠 [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh);批量从 audit 块灌入靠 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh);合法性由 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 把守。 - -## 1. JSONL Schema(一行一条) - -```json -{ - "time": "2026-05-08T14:30:00+0800", - "tool": "claude-code", - "session_id": null, - "prompt_summary": "搜索页快速输入结果串线", - "task_type": "concurrency", - "expected_rules": ["IR-005", "ROUTE-007", "SYM-003"], - "hit_rules": ["IR-005", "ROUTE-007"], - "missed_rules": ["SYM-003"], - "deviations": ["未明确取消旧请求链路"], - "outcome": "partial", - "evolution_signal": "修正表达" -} -``` - -| 字段 | 类型 | 必填 | 约束 | -|------|------|------|------| -| `time` | string | 是 | ISO8601 含时区,如 `2026-05-08T14:30:00+0800` | -| `tool` | string | 是 | 枚举:`codex` / `claude-code` / `cursor` / `manual` / `other` | -| `session_id` | string \| null | 是 | 三端可填会话 ID 便于回溯;不需要时填 `null` | -| `prompt_summary` | string | 是 | **摘要**,5-200 字符;禁贴原始 prompt、源码片段、可识别项目名 | -| `task_type` | string | 是 | 枚举:`layout` / `parameter-pass-through` / `concurrency` / `review` / `migration` / `mcp-control` / `other` | -| `expected_rules` | string[] | 是 | 元素必须是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID(如 `IR-005`) | -| `hit_rules` | string[] | 是 | 同上;可为空数组 | -| `missed_rules` | string[] | 是 | **必须等于** `expected_rules - hit_rules` 的集合差;append 脚本自动计算填入 | -| `deviations` | string[] | 是 | 自由文本数组,可为空数组 | -| `outcome` | string | 是 | 枚举:`pass` / `partial` / `fail` | -| `evolution_signal` | string | 是 | 枚举:`none` / `修正表达` / `新增能力` / `合并重复` / `退役规则`(与 [self_evolution.md](self_evolution.md) 的 4 种变更类型一致) | - -## 2. 写入协议(人/脚本通用) - -- **每个真实任务完成后追加一条**——无论成败。**平稳成功的任务也要记录**:只记败例会让 ledger 严重偏向负样本,命中率统计直接失真。 -- 同一会话有多个独立任务时,分多条记录(每条对应一个 task_type 判断)。 -- `prompt_summary` 必须脱敏: - - 不贴原始用户输入 - - 不贴源码片段或 stack trace - - 不贴包含可识别项目名的文件路径(除非项目本身公开) - - 5 字符下限保证至少有内容;200 字符上限保证不滥用 -- `expected_rules` 来源建议:先去 [rule_index.md](rule_index.md) 找匹配 `task_type` 的 ROUTE-XXX,再加上跨任务铁律(IR-002 求证 / IR-005 最小修复 / IR-008 残留风险声明等)。 -- `hit_rules` 必须诚实——如果不确定,**留空**而不是猜测填入。猜测会污染 Step 4 的命中率。 - -## 3. CLI 写入 - -```bash -bash scripts/append_usage_entry.sh \ - --tool claude-code \ - --task-type concurrency \ - --prompt-summary "搜索页快速输入结果串线" \ - --expected-rules "IR-005,ROUTE-007,SYM-003" \ - --hit-rules "IR-005,ROUTE-007" \ - --deviations "未明确取消旧请求链路" \ - --outcome partial \ - --evolution-signal "修正表达" -``` - -- 字段不合规直接非零退出,不污染 ledger -- `time` 自动取系统时间 -- `missed_rules` 自动从 `expected - hit` 计算,**不要手传** -- 可选:`--session-id ` / 省略 `--deviations`(默认空数组)/ 省略 `--evolution-signal`(默认 `none`) -- 持锁原子写入,并发安全 - -## 4. 三端 Audit 块格式(统一) - -任意工具(Codex CLI / Claude Code / Cursor)在合适时机输出如下文本块;之后由人工用 [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh) 批量灌入 ledger: - -``` - -tool: codex -task-type: concurrency -prompt-summary: 搜索页快速输入结果串线 -expected-rules: IR-005, ROUTE-007, SYM-003 -hit-rules: IR-005, ROUTE-007 -deviations: 未明确取消旧请求链路 -outcome: partial -evolution-signal: 修正表达 - -``` - -- 标签和字段名固定(kebab-case,与 JSONL 字段下划线版本对应) -- 数组字段用逗号分隔 -- 空数组:写空字符串(如 `deviations:`) -- `session-id` 可省,等价于 null -- 多个块之间用空行分隔,extract 脚本一次解析所有 - -## 5. 三端 system-prompt 片段(可粘贴) - -三端 system-prompt 各自加入下面对应段落。**核心约束统一**:仅在任务命中 ios-engineer 主题且 `task_type` 落在 6 个固定 slug + `other` 时才输出 audit 块;不要伪造 `hit-rules`,不确定就留空。 - -### 5.1 Codex CLI - -加到 `~/.codex/AGENTS.md` 或项目级 `AGENTS.md`: - -``` -## ios-engineer skill audit -当任务涉及 iOS / Swift / SwiftUI / UIKit / Xcode 工程,且 task_type 能落在 -{layout, parameter-pass-through, concurrency, review, migration, mcp-control, other} -之内时,在最终回答之后追加一个 块(格式见 ios-engineer skill -references/usage_ledger.md 第 4 节): -- tool: codex -- task-type: 上述 7 选 1 -- prompt-summary: 5-200 字符脱敏摘要 -- expected-rules / hit-rules: 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX 形式, - 来源是 ios-engineer/references/rule_index.md 的 active 集合 -- deviations: 偏离了什么;没有就留空 -- outcome: pass / partial / fail -- evolution-signal: none / 修正表达 / 新增能力 / 合并重复 / 退役规则 -不要伪造命中;不确定就在 hit-rules 里留空。 -``` - -### 5.2 Claude Code - -加到项目级 `CLAUDE.md` 或全局 `~/.claude/CLAUDE.md`: - -``` -## ios-engineer skill audit -完成任何 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务后,在回答末尾追加一个 - 块。格式严格遵守 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: claude-code -- task-type 只能落在 {layout, parameter-pass-through, concurrency, review, - migration, mcp-control, other} -- expected-rules / hit-rules 用 ios-engineer/references/rule_index.md 中 - status=active 的 ID -- 不确定 hit-rules 时留空,不要凭印象猜测 -- prompt-summary 脱敏,5-200 字符 -非 iOS 工程任务(写文档、看代码、答 API 问题)不必输出 audit 块。 -``` - -### 5.3 Cursor - -加到 `.cursorrules`: - -``` -## ios-engineer skill audit -对 iOS / Swift / SwiftUI / UIKit / Xcode 工程任务,回答之后追加 块, -格式见 ios-engineer/references/usage_ledger.md 第 4 节。 -- tool: cursor -- task-type ∈ {layout, parameter-pass-through, concurrency, review, migration, - mcp-control, other} -- expected-rules / hit-rules 用 IR-XXX / SYM-XXX / ROUTE-XXX / OUT-XXX -- 不确定就留空,不猜 -- prompt-summary 5-200 字符脱敏 -``` - -## 6. 批量灌入 - -```bash -bash scripts/extract_usage_audit.sh path/to/transcript.txt -``` - -- 抽取文件中所有 `...` 块 -- 解析 KV,逐块调 `append_usage_entry.sh` -- **任一块字段不全或字段非法 → 整批拒绝**,已写入条目不回滚(v1 受限),所以 extract 设计为 dry 校验全部通过后再统一写 -- 不做交互式确认;extract 是「audit 块作者的复制器」,不是审计员 - -## 7. 关于 self-grading 偏差的告示 - -**重要**:模型自己输出 audit 块本质上是 LLM 给自己评分。这会导致: - -- `hit_rules` 系统性高估(模型倾向于声称自己做到了) -- `deviations` 系统性低估(模型不容易察觉自己的偏离) -- 同一个模型在「执行任务」与「审计任务」两个角色里有共同盲点 - -**所以本 ledger 的数据是「有偏的草稿」**,不是 ground truth。真正可信的命中率要靠 [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) 的回归场景集独立回放确认。 - -Step 4 的 summarize 脚本会按 `tool` 字段分桶,让不同工具间的 self-grading 偏差互相暴露——这是 ledger 现阶段最有用的次级诊断。 - -## 8. 提案候选信号阈值 - -[scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 硬编码 4 个阈值常量,超过即在 summarize 输出中作为提案候选信号浮出。本节是这 4 个常量的文档化镜像: - -| 常量 | 值 | 候选提案信号 | 含义 | -|------|----|-------------|------| -| `MISSED_RULE_THRESHOLD` | 3 | 新增能力 | 同一 `rule_id` 在 `missed_rules` 中累计 ≥ 3 次 → 现有规则可能表达不到位或缺触发条件 | -| `TASK_TYPE_OTHER_THRESHOLD` | 5 | 新增能力(新 task_type) | `task_type=other` 累计 ≥ 5 条 → 现有 6 个 slug 覆盖不全,可能需新增场景 | -| `DEVIATION_THRESHOLD` | 2 | 修正表达 | 同一 deviation 字符串重复 ≥ 2 次 → 稳定失败模式,对应规则需收紧表达 | -| `TOOL_DIVERGENCE_THRESHOLD` | 0.4 | self-grading 偏差对比 | 同一 `rule_id` 在不同 `tool` 间 hit_rate 差异 ≥ 40%(且每端 expected ≥ 5) → 工具/模型对规则理解分裂,需独立回放确认 | - -**漂移防护**:阈值与 [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) 的 `*_THRESHOLD` 常量一一对应。改本文必须同时改脚本,否则 summarize 输出(`thresholds` 字段会带脚本真值)与文档解释会漂移。后续提案可考虑把"脚本常量 ↔ 本表数字"双向校验补到 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh)。 - -## 9. 维护 - -- 新增 `task_type` 枚举值:先扩 [validation_scenarios.md](validation_scenarios.md) 与 [evolution/scenarios/](../evolution/scenarios/),再同步 [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh) 与本文件。 -- 新增 `tool` 枚举值(如 Aider / Continue 等):直接改本文件 + `validate_usage_ledger.sh` + `append_usage_entry.sh` 的白名单。 -- ledger 体积超大(> 10k 行)时再考虑分片或压缩归档;Step 3 不预留分片机制。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/validation_scenarios.md deleted file mode 100644 index 2c99b19..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,166 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 -- 结构化定义沉淀在 [evolution/scenarios/](../evolution/scenarios/) 下的 6 份 JSON 规格(`expected_hits` / `failure_signals` / `output_contract` / `primary_refs`),本文件作为人读伴随。新增或调整场景时**先改 JSON,后同步本文**;伞形校验 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) 会断言两侧 slug 一致、字段齐全。 - -### JSON 与本文同步流程 -按以下顺序执行,跳步会被对应脚本捕获: - -1. 改 `evolution/scenarios/.json` 的 `expected_hits[].rule_id` / `failure_signals[].rule_id` / `output_contract` / `primary_refs`。 -2. 跑 [scripts/validate_scenario_specs.sh](../scripts/validate_scenario_specs.sh) — 断言 6 份 JSON 与本文 slug 双向一致、字段齐全。漏跑会让 slug 漂移在 grader 阶段才暴露。 -3. 同步本文对应场景描述("用户输入示例 / 通过标准 / 失败信号")。 -4. 跑 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) — 断言 JSON 内 `rule_id` 是 [rule_index.md](rule_index.md) 中 `status=active` 的 ID。漏跑会让 retired/deprecated/不存在的 ID 进入场景规格。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 -- 输出在“结论”段之前含独立的“版本前提”块(按 examples.md §4 模板),写出真值或显式假设(IR-006)。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 -- 给出并发 / 可用性 API / SwiftUI 行为建议但既无真值也无显式假设,隐性使用某个 iOS / Swift 版本的 API。 -- 未把版本前提作为独立块字面输出,仅在散文里隐含。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 -- 输出末尾含独立的“残留风险声明”块,固定三字段:已覆盖 / 未覆盖 / 残留风险,作为独立段落字面存在,不与“验证缺口”合并(IR-008)。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 -- 残留风险声明缺失、三字段不全、或被合并进“验证缺口”段。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` - -可选字段(场景规格 JSON 中的 `expected_hits[]` / `failure_signals[]`): - -- `rule_id`:填 SKILL.md 中已存在的 active ID(如 `IR-005`),用于跨场景统计命中频率与 missed_rules 列表对账;ID 来源见 [rule_index.md](rule_index.md),校验由 [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) 把守。 diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/append_usage_entry.sh deleted file mode 100755 index 6a0b8a7..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/append_usage_entry.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/append_usage_entry.sh \ - --tool \ - --task-type \ - --prompt-summary "<5-200 char Chinese summary>" \ - --expected-rules "ID1,ID2,..." \ - --hit-rules "ID1,..." \ - [--deviations "txt1;txt2;..."] \ - [--outcome ] \ - [--evolution-signal ] \ - [--session-id ] -USAGE - exit 1 -} - -tool="" -task_type="" -prompt_summary="" -expected_rules_raw="" -hit_rules_raw="" -deviations_raw="" -outcome="pass" -evolution_signal="none" -session_id="" - -while [ $# -gt 0 ]; do - case "$1" in - --tool) tool="$2"; shift 2 ;; - --task-type) task_type="$2"; shift 2 ;; - --prompt-summary) prompt_summary="$2"; shift 2 ;; - --expected-rules) expected_rules_raw="$2"; shift 2 ;; - --hit-rules) hit_rules_raw="$2"; shift 2 ;; - --deviations) deviations_raw="$2"; shift 2 ;; - --outcome) outcome="$2"; shift 2 ;; - --evolution-signal) evolution_signal="$2"; shift 2 ;; - --session-id) session_id="$2"; shift 2 ;; - -h|--help) usage ;; - *) echo "Unknown arg: $1"; usage ;; - esac -done - -if [ -z "$tool" ] || [ -z "$task_type" ] || [ -z "$prompt_summary" ] || [ -z "$expected_rules_raw" ] || [ -z "$hit_rules_raw" ]; then - echo "Missing required argument." - usage -fi - -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -now="$(date '+%Y-%m-%dT%H:%M:%S%z')" - -ruby -rjson - "$tool" "$task_type" "$prompt_summary" "$expected_rules_raw" "$hit_rules_raw" "$deviations_raw" "$outcome" "$evolution_signal" "$session_id" "$now" "$RULE_INDEX_FILE" "$LEDGER_FILE" <<'RUBY' -tool, task_type, prompt_summary, expected_raw, hit_raw, deviations_raw, -outcome, evolution_signal, session_id, now, rule_index_path, ledger_path = ARGV - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].freeze -ALLOWED_OUTCOMES = %w[pass partial fail].freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -errors = [] - -errors << "tool '#{tool}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(tool) -errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(task_type) -errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) -errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) - -# prompt_summary length 5-200 (chars, not bytes) -ps_len = prompt_summary.length -errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) - -# Active rule_id set from rule_index.md -active_ids = [] -File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end -active_set = active_ids.to_set rescue active_ids - -split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } - -expected_rules = split.call(expected_raw) -hit_rules = split.call(hit_raw) -deviations = deviations_raw.split(";").map(&:strip).reject(&:empty?) - -(expected_rules + hit_rules).each do |rid| - errors << "rule_id '#{rid}' violates format ^[A-Z]+-\\d{3}$" unless rid =~ ID_FORMAT - next unless rid =~ ID_FORMAT - unless active_ids.include?(rid) - errors << "rule_id '#{rid}' not in rule_index.md active set" - end -end - -unless errors.empty? - warn "append_usage_entry validation failed:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules -missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } - -entry = { - "time" => now, - "tool" => tool, - "session_id" => session_id.empty? ? nil : session_id, - "prompt_summary" => prompt_summary, - "task_type" => task_type, - "expected_rules" => expected_rules, - "hit_rules" => hit_rules, - "missed_rules" => missed_rules, - "deviations" => deviations, - "outcome" => outcome, - "evolution_signal" => evolution_signal -} - -line = JSON.generate(entry) -File.open(ledger_path, "a") { |f| f.puts(line) } -puts line -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100755 index dfe75cd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -if [[ ! "$approved_by" =~ ^[A-Za-z0-9_@.-]{1,100}$ ]]; then - echo "Invalid approved_by format (expected ^[A-Za-z0-9_@.-]{1,100}$): ${approved_by}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -# 用 ruby JSON.pretty_generate 安全写入 -ruby -rjson -e ' - data = { - "proposal_id" => ARGV[0], - "proposal_file" => ARGV[1], - "approved_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "approved_by" => ARGV[2], - "status" => "approved" - } - File.write(ARGV[3], JSON.pretty_generate(data) + "\n") -' "$proposal_id" "$proposal_file" "$approved_by" "$approval_file" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" approved >/dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100755 index 6382061..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat </dev/null 2>&1; then - echo "Drift: ${rel}" - drift=1 - fi - else - local diff_out - diff_out="$(diff -rq "$snapshot_path" "$current_path" 2>&1 || true)" - if [ -n "$diff_out" ]; then - echo "$diff_out" | sed "s|^|Drift: |" - drift=1 - fi - fi -} - -check_path "SKILL.md" -check_path "agents" -check_path "references" -check_path "scripts" - -if [ "$drift" -ne 0 ]; then - echo "Snapshot consistency FAILED: working tree differs from active snapshot ${active_version}" - echo "Hint: if this drift is intentional, promote a new version via the proposal flow." - exit 1 -fi - -echo "Snapshot consistency OK: active=${active_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/create_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/create_skill_proposal.sh deleted file mode 100755 index f919082..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/create_skill_proposal.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/create_skill_proposal.sh " - exit 1 -fi - -slug="$1" - -if [[ ! "$slug" =~ ^[A-Za-z0-9_-]{1,80}$ ]]; then - echo "Invalid slug format (expected ^[A-Za-z0-9_-]{1,80}$): ${slug}" - exit 1 -fi - -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" <" - echo "Parses all ... blocks and appends them to evolution/usage/usage.jsonl." - echo "Atomic: any block invalid -> entire batch rejected, ledger untouched." - exit 1 -fi - -input="$1" - -if [ ! -f "$input" ]; then - echo "Input file not found: ${input}" - exit 1 -fi - -LEDGER_FILE="evolution/usage/usage.jsonl" -LOCK_DIR="evolution/usage/usage.jsonl.lock" -mkdir -p "$(dirname "$LEDGER_FILE")" -[ -f "$LEDGER_FILE" ] || : > "$LEDGER_FILE" - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$LOCK_DIR" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$LOCK_DIR" ]; then - echo "Failed to acquire ledger lock: ${LOCK_DIR}" - exit 1 -fi - -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } -trap cleanup EXIT - -ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - -input_path, ledger_path = ARGV -text = File.read(input_path) -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } - -if blocks.empty? - puts "No blocks found in #{input_path}" - exit 0 -end - -REQUIRED_KEYS = %w[tool task-type prompt-summary expected-rules hit-rules outcome evolution-signal].freeze - -errors = [] -parsed = [] - -blocks.each_with_index do |body, idx| - block_no = idx + 1 - data = {} - body.each_line do |raw_line| - line = raw_line.strip - next if line.empty? - if (m = line.match(/\A([a-z][a-z-]*):\s*(.*)\z/)) - data[m[1]] = m[2] - else - errors << "block #{block_no}: line '#{line}' does not match 'key: value'" - end - end - - REQUIRED_KEYS.each do |k| - errors << "block #{block_no}: missing key '#{k}'" unless data.key?(k) - end - next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) - - ps = data["prompt-summary"] - unless ps.length.between?(5, 200) - errors << "block #{block_no}: prompt-summary length must be 5-200 chars (got #{ps.length})" - end - - expected = data["expected-rules"].split(",").map(&:strip).reject(&:empty?) - hit = data["hit-rules"].split(",").map(&:strip).reject(&:empty?) - (expected + hit).each do |rid| - unless rid =~ ID_FORMAT - errors << "block #{block_no}: rule_id '#{rid}' violates format" - next - end - unless active_ids.include?(rid) - errors << "block #{block_no}: rule_id '#{rid}' not in rule_index.md active set" - end - end - - deviations = (data["deviations"] || "").split(";").map(&:strip).reject(&:empty?) - session_id_raw = data["session-id"] - session_id = (session_id_raw.nil? || session_id_raw.strip.empty?) ? nil : session_id_raw.strip - - parsed << { - "tool" => data["tool"], - "session_id" => session_id, - "prompt_summary" => ps, - "task_type" => data["task-type"], - "expected_rules" => expected, - "hit_rules" => hit, - "missed_rules" => expected.reject { |r| hit.include?(r) }, - "deviations" => deviations, - "outcome" => data["outcome"], - "evolution_signal" => data["evolution-signal"] - } -end - -unless errors.empty? - warn "Extract failed; ledger NOT modified:" - errors.each { |e| warn " - #{e}" } - exit 1 -end - -now = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.open(ledger_path, "a") do |f| - parsed.each do |entry| - f.puts(JSON.generate({ "time" => now }.merge(entry))) - end -end - -puts "Appended #{parsed.length} entries to #{ledger_path}" -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/promote_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/promote_skill_evolution.sh deleted file mode 100755 index e9f174d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/promote_skill_evolution.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/promote_skill_evolution.sh [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" - -# 字段白名单校验 -if [[ ! "$new_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid new_version format (expected ^v[0-9]+(-[A-Za-z0-9]+)*$): ${new_version}" - exit 1 -fi - -if [[ ! "$source_ref" =~ ^[A-Za-z0-9:_./-]{1,200}$ ]]; then - echo "Invalid source_ref format (expected ^[A-Za-z0-9:_./-]{1,200}$): ${source_ref}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 - fi -fi - -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -# 用 ruby JSON.pretty_generate 安全写入 metadata -ruby -rjson -e ' - data = { - "version" => ARGV[0], - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1] - } - File.write(ARGV[2], JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" "${history_dir}/metadata.json" - -# 用 ruby JSON.pretty_generate 安全写入 active_version -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => ARGV[1], - "notes" => "Promoted after passing base evolution validation." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$new_version" "$source_ref" - -if [ -n "$proposal_file" ]; then - bash scripts/update_skill_proposal_status.sh "$proposal_file" promoted >/dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/record_validation_scenario.sh deleted file mode 100755 index e91f203..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100755 index bdd5eb6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" - -# 1. 版本格式白名单 -if [[ ! "$target_version" =~ ^v[0-9]+(-[A-Za-z0-9]+)*$ ]]; then - echo "Invalid version format (must match ^v[0-9]+(-[A-Za-z0-9]+)*$): ${target_version}" - exit 1 -fi - -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -# 2. snapshot 完整性预检查 -required=("SKILL.md" "agents" "references" "scripts") -for p in "${required[@]}"; do - if [ ! -e "${snapshot_dir}/${p}" ]; then - echo "Snapshot incomplete, missing: ${snapshot_dir}/${p}" - exit 1 - fi -done - -# 3. snapshot 复制到临时目录 + 基础预校验 -stage_dir="$(mktemp -d)" -cleanup_stage() { rm -rf "$stage_dir"; } -trap cleanup_stage EXIT - -cp "${snapshot_dir}/SKILL.md" "${stage_dir}/SKILL.md" -cp -R "${snapshot_dir}/agents" "${stage_dir}/agents" -cp -R "${snapshot_dir}/references" "${stage_dir}/references" -cp -R "${snapshot_dir}/scripts" "${stage_dir}/scripts" - -ruby -e 'require "yaml"; YAML.load_file(ARGV[0])' "${stage_dir}/SKILL.md" \ - || { echo "Staged SKILL.md YAML invalid"; exit 1; } - -staged_lines="$(wc -l < "${stage_dir}/SKILL.md" | tr -d ' ')" -if [ "$staged_lines" -gt 500 ]; then - echo "Staged SKILL.md too long: ${staged_lines} lines" - exit 1 -fi - -# 4. 当前文件移到备份,再把暂存区 move 成正式位置;失败自动恢复 -backup_dir="$(mktemp -d)" -restore_backup() { - for item in SKILL.md agents references scripts; do - if [ -e "${backup_dir}/${item}" ]; then - rm -rf "${item}" - mv "${backup_dir}/${item}" "./${item}" - fi - done -} - -trap 'restore_backup; cleanup_stage; rm -rf "$backup_dir"' ERR - -for item in SKILL.md agents references scripts; do - if [ -e "$item" ]; then - mv "$item" "${backup_dir}/${item}" - fi -done - -mv "${stage_dir}/SKILL.md" SKILL.md -mv "${stage_dir}/agents" agents -mv "${stage_dir}/references" references -mv "${stage_dir}/scripts" scripts - -# 5. 完整 validate -if ! bash scripts/validate_skill_evolution.sh; then - echo "Validation failed after rollback. Restoring backup..." - for item in SKILL.md agents references scripts; do - rm -rf "$item" - if [ -e "${backup_dir}/${item}" ]; then - mv "${backup_dir}/${item}" "./${item}" - fi - done - rm -rf "$backup_dir" - exit 1 -fi - -# 6. active_version.json 通过 ruby JSON 序列化 -ruby -rjson -e ' - data = { - "active_version" => ARGV[0], - "status" => "active", - "promoted_at" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"), - "source" => "rollback", - "notes" => "Rolled back to archived stable snapshot." - } - File.write("evolution/active_version.json", JSON.pretty_generate(data) + "\n") -' "$target_version" - -# 7. 清理备份 -rm -rf "$backup_dir" -trap - ERR - -echo "Rolled back to ${target_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/run_behavior_validation.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/run_behavior_validation.sh deleted file mode 100755 index 7c68fd9..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/run_behavior_validation.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[behavior 1/5] Active snapshot consistency" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[behavior 2/5] Proposal script rejection paths" -bash scripts/test_proposal_scripts.sh - -echo "[behavior 3/5] Repository template usability" -ruby <<'RUBY' -require "tmpdir" - -content = File.read("references/code_templates.md") -section = content[/## Repository 模板.*?(?=\n## APIClient 模板)/m] -abort("Missing Repository template section") unless section - -code = section[/```swift\n(.*?)\n```/m, 1] -abort("Missing Repository template Swift block") unless code - -required_fragments = { - "logger field" => "private let logger: LoggerProtocol", - "logger init parameter" => "logger: LoggerProtocol", - "logger assignment" => "self.logger = logger", - "cache read logging" => "logger.error(\"cache read failed", - "cache write logging" => "logger.error(\"cache write failed" -} - -missing = required_fragments.select { |_label, text| !code.include?(text) } -unless missing.empty? - missing.each { |label, _text| warn "Missing Repository template fragment: #{label}" } - exit 1 -end - -if code.include?("try? cache.read") || code.include?("try? cache.write") - warn "Repository template regressed to silent cache errors" - exit 1 -end - -tmp = File.join(Dir.mktmpdir, "RepositoryTemplate.swift") -File.write(tmp, <<~SWIFT) - import Foundation - - struct FeatureEntity {} - - protocol FeatureRemoteDataSourceProtocol { - func fetch() async throws -> FeatureEntity - } - - protocol FeatureCacheProtocol { - func read() throws -> FeatureEntity? - func write(_ entity: FeatureEntity) throws - } - - protocol LoggerProtocol { - func error(_ message: String) - } - - #{code} -SWIFT - -if system("command -v swiftc >/dev/null 2>&1") - cache_dir = File.join(Dir.tmpdir, "ios-engineer-swift-module-cache") - Dir.mkdir(cache_dir) unless Dir.exist?(cache_dir) - unless system("swiftc", "-module-cache-path", cache_dir, "-typecheck", tmp) - warn "Repository template Swift typecheck failed" - exit 1 - end -else - warn "swiftc not found; skipped Repository template typecheck after textual checks" -end -RUBY - -echo "[behavior 4/5] Code review output contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -review = File.read("references/review_checklists.md") -examples = File.read("references/examples.md") - -unless skill.include?("代码审查 / PR Review 例外") && - skill.include?("findings-first") && - skill.include?("[review_checklists.md](references/review_checklists.md)") - warn "SKILL.md no longer routes code review to findings-first review_checklists.md" - exit 1 -end - -required_sections = ["审查结论", "严重问题", "一般问题", "验证缺口", "最终要求"] -missing = required_sections.reject { |section| review.include?(section) } -unless missing.empty? - warn "review_checklists.md missing findings-first section(s): #{missing.join(', ')}" - exit 1 -end - -if examples =~ /代码审查[\s\S]{0,300}根因\s*[-→>].*为什么\s*[-→>].*修法\s*[-→>].*验证/m - warn "examples.md appears to redefine code review as root-cause four-step output" - exit 1 -end -RUBY - -echo "[behavior 5/5] Network cache and error-modeling contract" -ruby <<'RUBY' -skill = File.read("SKILL.md") -network = File.read("references/networking_patterns.md") -domain = File.read("references/domain_modeling.md") -templates = File.read("references/code_templates.md") - -unless skill.include?("请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染") && - skill.include?("[networking_patterns.md](references/networking_patterns.md)") && - skill.include?("错误建模追加 [domain_modeling.md](references/domain_modeling.md)") - warn "SKILL.md no longer routes network/cache issues to networking_patterns.md plus domain_modeling.md" - exit 1 -end - -unless network.include?("缓存模式") && - network.include?("必须定义缓存键") && - network.include?("不得让 ViewModel 直接感知缓存实现细节") - warn "networking_patterns.md missing cache behavior constraints" - exit 1 -end - -unless domain.include?("ErrorModel") && - domain.include?("传输错误") && - domain.include?("状态码错误") && - domain.include?("解码错误") && - domain.include?("鉴权错误") && - domain.include?("业务错误") && - domain.include?("展示错误") - warn "domain_modeling.md missing ErrorModel layered error contract" - exit 1 -end - -if templates.include?("try? cache.read") || templates.include?("try? cache.write") - warn "code_templates.md regressed to silent cache errors" - exit 1 -end - -unless templates.include?("缓存读失败不得压成单一 nil 分支") && - templates.include?("缓存写失败必须记录") - warn "code_templates.md missing explicit cache failure behavior" - exit 1 -end -RUBY - -echo "Behavior validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/summarize_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/summarize_usage_ledger.sh deleted file mode 100755 index 4ff48ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/summarize_usage_ledger.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -usage() { - cat <<'USAGE' -Usage: bash scripts/summarize_usage_ledger.sh [options] - -Aggregate evolution/usage/usage.jsonl into a human-readable summary plus -proposal-candidate signals. Read-only: never modifies the ledger or repo. - -Options: - --since YYYY-MM-DD Only include entries with time >= this date - --tool Only include entries with this tool value - --json Emit JSON instead of markdown - --output FILE Write to FILE instead of stdout - -h, --help Show this help - -Thresholds (hardcoded, change in source if needed): - missed_rule >= 3 => surfaced as proposal signal - task_type=other >= 5 => surfaced as missing-scenario signal - deviation count >= 2 => surfaced as stable failure mode - tool hit_rate diff >= 0.4 (each tool >= 5 expected for that rule) - => surfaced as tool divergence -USAGE -} - -since="" -tool_filter="" -emit_json=0 -output_path="" - -while [ $# -gt 0 ]; do - case "$1" in - --since) since="$2"; shift 2 ;; - --tool) tool_filter="$2"; shift 2 ;; - --json) emit_json=1; shift ;; - --output) output_path="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;; - esac -done - -if [ ! -f "$LEDGER_FILE" ]; then - msg="No entries yet (ledger empty: ${LEDGER_FILE} missing)" - if [ -n "$output_path" ]; then echo "$msg" > "$output_path"; else echo "$msg"; fi - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" >&2 - exit 1 -fi - -ruby - "$LEDGER_FILE" "$RULE_INDEX_FILE" "$since" "$tool_filter" "$emit_json" "$output_path" <<'RUBY' -require "json" -require "date" - -ledger_path, index_path, since_str, tool_filter, emit_json_str, output_path = ARGV -emit_json = emit_json_str == "1" - -# Thresholds -MISSED_RULE_THRESHOLD = 3 -TASK_TYPE_OTHER_THRESHOLD = 5 -DEVIATION_THRESHOLD = 2 -TOOL_DIVERGENCE_THRESHOLD = 0.4 -MIN_TOOL_SAMPLE_SIZE = 5 - -# Build rule_id -> summary map -rule_summary = {} -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|\s*([^|]+?)\s*\|/) - rule_summary[m[1]] = m[2].strip if m -end - -since_date = since_str.empty? ? nil : (Date.parse(since_str) rescue nil) -if !since_str.empty? && since_date.nil? - warn "Invalid --since '#{since_str}', expected YYYY-MM-DD" - exit 1 -end - -raw_entries = [] -malformed = 0 -File.foreach(ledger_path).with_index(1) do |line, lineno| - line = line.strip - next if line.empty? - begin - raw_entries << JSON.parse(line) - rescue JSON::ParserError - warn "line #{lineno}: malformed JSON skipped (run validate_usage_ledger.sh to repair)" - malformed += 1 - end -end - -# Apply filters -entries = raw_entries.select do |e| - next false if tool_filter != "" && e["tool"] != tool_filter - if since_date - begin - ed = Date.parse(e["time"]) - next false if ed < since_date - rescue - next false - end - end - true -end - -if entries.empty? - msg = - if raw_entries.empty? - "No entries yet (ledger empty)" - else - "No entries match filter (since=#{since_str.empty? ? '*' : since_str}, tool=#{tool_filter.empty? ? '*' : tool_filter})" - end - out = output_path.empty? ? $stdout : File.open(output_path, "w") - out.puts(msg) - out.close unless out == $stdout - exit 0 -end - -# --- Aggregations --- -by_tool = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0, "partial" => 0, "fail" => 0 } } -by_task_type = Hash.new { |h, k| h[k] = { "entries" => 0, "pass" => 0 } } -rule_stats = Hash.new { |h, k| h[k] = { "expected" => 0, "hit" => 0, "miss" => 0 } } -deviation_counts = Hash.new(0) -tool_rule_hit = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = { "expected" => 0, "hit" => 0 } } } - -entries.each do |e| - tool = e["tool"] - by_tool[tool]["entries"] += 1 - by_tool[tool][e["outcome"]] += 1 if %w[pass partial fail].include?(e["outcome"]) - - by_task_type[e["task_type"]]["entries"] += 1 - by_task_type[e["task_type"]]["pass"] += 1 if e["outcome"] == "pass" - - expected = e["expected_rules"] || [] - hit = e["hit_rules"] || [] - missed = e["missed_rules"] || [] - - expected.each do |rid| - rule_stats[rid]["expected"] += 1 - tool_rule_hit[tool][rid]["expected"] += 1 - end - hit.each do |rid| - rule_stats[rid]["hit"] += 1 - tool_rule_hit[tool][rid]["hit"] += 1 - end - missed.each { |rid| rule_stats[rid]["miss"] += 1 } - - (e["deviations"] || []).each { |d| deviation_counts[d] += 1 } -end - -# --- Proposal signals --- -signals = [] - -# 1. Missed rule frequency -missed_freq = rule_stats.select { |_, v| v["miss"] >= MISSED_RULE_THRESHOLD } - .sort_by { |_, v| -v["miss"] } -missed_freq.each do |rid, v| - signals << { - "kind" => "missed_rule", - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "miss_count" => v["miss"], - "note" => "#{rid} (#{rule_summary[rid] || '(unknown)'}) 在 #{v['miss']} 个任务中 missed —— 规则表达不清或路由不够触发?建议 review 该规则与对应 ref。" - } -end - -# 2. task_type=other -other_count = (by_task_type["other"] || {})["entries"] || 0 -if other_count >= TASK_TYPE_OTHER_THRESHOLD - signals << { - "kind" => "task_type_other", - "count" => other_count, - "note" => "task_type=other 累计 #{other_count} 条 —— 当前 6 个固定场景可能漏覆盖了一类常见任务,建议看 prompt_summary 找模式后扩 validation_scenarios.md。" - } -end - -# 3. Deviation frequency -hot_deviations = deviation_counts.select { |_, c| c >= DEVIATION_THRESHOLD } - .sort_by { |_, c| -c } -hot_deviations.each do |text, c| - signals << { - "kind" => "deviation", - "text" => text, - "count" => c, - "note" => "「#{text}」出现 #{c} 次 —— 稳定失败模式,建议在相关 ref 加更明确的检查项。" - } -end - -# 4. Tool divergence -tools_present = tool_rule_hit.keys -divergence = [] -all_rule_ids = rule_stats.keys -all_rule_ids.each do |rid| - rates = [] - tools_present.each do |t| - expected = tool_rule_hit[t][rid]["expected"] - hit = tool_rule_hit[t][rid]["hit"] - next if expected < MIN_TOOL_SAMPLE_SIZE - rate = hit.to_f / expected - rates << [t, rate, expected, hit] - end - next if rates.length < 2 - rates.sort_by! { |_, r, _, _| -r } - high = rates.first - low = rates.last - diff = (high[1] - low[1]).abs - next if diff < TOOL_DIVERGENCE_THRESHOLD - divergence << { - "rule_id" => rid, - "summary" => rule_summary[rid] || "(unknown)", - "high_tool" => high[0], - "high_rate" => (high[1] * 100).round(0), - "low_tool" => low[0], - "low_rate" => (low[1] * 100).round(0), - "diff_pct" => (diff * 100).round(0) - } -end -divergence.sort_by! { |d| -d["diff_pct"] } -divergence.each do |d| - signals << { - "kind" => "tool_divergence", - "rule_id" => d["rule_id"], - "summary" => d["summary"], - "note" => "#{d['rule_id']} 在 #{d['high_tool']} 命中率 #{d['high_rate']}%,#{d['low_tool']} 命中率 #{d['low_rate']}%(差 #{d['diff_pct']}%)—— 工具差异显著,可能 prompt 注入语境不同或一端做了更深的求证。", - "data" => d - } -end - -# --- Time window summary --- -times = entries.map { |e| Date.parse(e["time"]) rescue nil }.compact.sort -window_from = times.first&.to_s || "?" -window_to = times.last&.to_s || "?" - -summary = { - "window_from" => window_from, - "window_to" => window_to, - "tool_filter" => tool_filter.empty? ? "all" : tool_filter, - "since_filter" => since_str.empty? ? "*" : since_str, - "total_entries" => entries.length, - "malformed_lines" => malformed, - "thresholds" => { - "missed_rule" => MISSED_RULE_THRESHOLD, - "task_type_other" => TASK_TYPE_OTHER_THRESHOLD, - "deviation" => DEVIATION_THRESHOLD, - "tool_divergence" => TOOL_DIVERGENCE_THRESHOLD, - "min_tool_sample_size" => MIN_TOOL_SAMPLE_SIZE - }, - "by_tool" => by_tool.sort_by { |_, v| -v["entries"] }.to_h, - "by_task_type" => by_task_type.sort_by { |_, v| -v["entries"] }.to_h, - "rule_stats" => rule_stats.sort_by { |_, v| -v["expected"] }.to_h, - "top_missed" => missed_freq.map { |rid, v| { "rule_id" => rid, "summary" => rule_summary[rid] || "(unknown)", "miss_count" => v["miss"] } }, - "top_deviations" => hot_deviations.map { |t, c| { "text" => t, "count" => c } }, - "proposal_signals" => signals -} - -# --- Render --- -def pct(part, total) - return "—" if total == 0 - "#{(part.to_f / total * 100).round(0)}%" -end - -if emit_json - rendered = JSON.pretty_generate(summary) -else - lines = [] - lines << "# Usage Ledger Summary" - lines << "- 时间窗:#{summary['window_from']} ~ #{summary['window_to']}" - lines << "- 时间过滤:#{summary['since_filter']}" - lines << "- 工具过滤:#{summary['tool_filter']}" - lines << "- 总条目:#{summary['total_entries']}" - lines << "- 跳过非法行:#{summary['malformed_lines']}" if summary["malformed_lines"] > 0 - lines << "" - - lines << "## 按工具" - lines << "" - lines << "| tool | entries | pass% | partial% | fail% |" - lines << "|------|---------|-------|----------|-------|" - summary["by_tool"].each do |t, v| - lines << "| #{t} | #{v['entries']} | #{pct(v['pass'], v['entries'])} | #{pct(v['partial'], v['entries'])} | #{pct(v['fail'], v['entries'])} |" - end - lines << "" - - lines << "## 按 task_type" - lines << "" - lines << "| task_type | entries | pass% |" - lines << "|-----------|---------|-------|" - summary["by_task_type"].each do |tt, v| - line = "| #{tt} | #{v['entries']} | #{pct(v['pass'], v['entries'])} |" - line += " ← signal" if tt == "other" && v["entries"] >= TASK_TYPE_OTHER_THRESHOLD - lines << line - end - lines << "" - - lines << "## 命中频率(按 expected 出现次数降序)" - lines << "" - lines << "| rule_id | 摘要 | expected | hit | hit_rate |" - lines << "|---------|------|----------|-----|----------|" - summary["rule_stats"].each do |rid, v| - rate = v["expected"] == 0 ? "—" : "#{(v['hit'].to_f / v['expected'] * 100).round(0)}%" - lines << "| #{rid} | #{rule_summary[rid] || '(unknown)'} | #{v['expected']} | #{v['hit']} | #{rate} |" - end - lines << "" - - unless summary["top_missed"].empty? - lines << "## Top missed rules(miss count ≥ #{MISSED_RULE_THRESHOLD})" - lines << "" - lines << "| rule_id | 摘要 | miss count |" - lines << "|---------|------|------------|" - summary["top_missed"].each do |m| - lines << "| #{m['rule_id']} | #{m['summary']} | #{m['miss_count']} |" - end - lines << "" - end - - unless summary["top_deviations"].empty? - lines << "## Top deviations(count ≥ #{DEVIATION_THRESHOLD},完全相等聚合)" - lines << "" - lines << "| 偏差描述 | count |" - lines << "|----------|-------|" - summary["top_deviations"].each do |d| - lines << "| #{d['text']} | #{d['count']} |" - end - lines << "" - end - - lines << "## 提案候选信号" - lines << "" - lines << "> 阈值:missed_rule ≥ #{MISSED_RULE_THRESHOLD} / task_type=other ≥ #{TASK_TYPE_OTHER_THRESHOLD} / 同一 deviation ≥ #{DEVIATION_THRESHOLD} / 工具间 hit_rate 差 ≥ #{(TOOL_DIVERGENCE_THRESHOLD * 100).round(0)}%(每端最少 #{MIN_TOOL_SAMPLE_SIZE} 条样本)" - lines << "> 触发不等于必须建提案;只是值得一看。" - lines << "" - if signals.empty? - lines << "_(暂无超过阈值的信号)_" - else - signals.each do |s| - lines << "- ⚠️ #{s['note']}" - end - end - - rendered = lines.join("\n") + "\n" -end - -if output_path.empty? - print rendered -else - File.write(output_path, rendered) - warn "Wrote #{rendered.bytesize} bytes to #{output_path}" -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/test_proposal_scripts.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/test_proposal_scripts.sh deleted file mode 100755 index 68f7866..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/test_proposal_scripts.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash - -# 测试 proposal 脚本的入参拒绝路径。 -# 聚焦 regex 白名单一致性;不做文件副作用断言。 -# 用法:bash scripts/test_proposal_scripts.sh -# 失败退出非零并打印首个失败用例。 - -set -u - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -fail=0 -pass=0 - -expect_reject() { - local label="$1"; shift - local expect_msg="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -eq 0 ]; then - echo "FAIL: ${label} should have rejected but exit=0" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - if ! printf '%s' "$out" | grep -q -- "$expect_msg"; then - echo "FAIL: ${label} rejected but message did not contain '${expect_msg}'" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -expect_ok() { - local label="$1"; shift - local out rc - out="$("$@" 2>&1)" - rc=$? - if [ "$rc" -ne 0 ]; then - echo "FAIL: ${label} should have succeeded but exit=${rc}" - echo " cmd: $*" - echo " out: ${out}" - fail=$((fail+1)) - return - fi - pass=$((pass+1)) -} - -# ---- create_skill_proposal.sh slug whitelist ---- -for slug in "fix root" "../../../etc/passwd" "修复" "fix/root" "fix.v2" "" "$(printf 'a%.0s' {1..81})"; do - expect_reject "create rejects slug: '${slug}'" "Invalid slug format" \ - bash scripts/create_skill_proposal.sh "$slug" -done - -# ---- proposal_file whitelist on all consuming scripts ---- -BAD_PATHS=( - "/etc/hosts" - "../../../etc/passwd" - "evolution/proposals/foo.md" - "evolution/proposals/20260101-foo.md" - "evolution/proposals/20260101-000000-.md" -) - -for bad in "${BAD_PATHS[@]}"; do - expect_reject "approve rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/approve_skill_promotion.sh "$bad" approved-by-test - expect_reject "promote rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/promote_skill_evolution.sh v999 proposal:test "$bad" - expect_reject "validate rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/validate_skill_proposal.sh "$bad" - expect_reject "record rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/record_validation_scenario.sh "$bad" layout pass a b c - expect_reject "update-status rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/update_skill_proposal_status.sh "$bad" draft - expect_reject "check-readiness rejects: ${bad}" "Invalid proposal_file format" \ - bash scripts/check_skill_promotion_readiness.sh "$bad" -done - -# ---- snapshot consistency: must report OK when tree matches active snapshot ---- -# 此脚本可能在晋升前(漂移态)或晋升后(一致态)运行。 -# 用 SKIP 绕过以验证校验分支本身能正常加载脚本;真实一致性的断言留到 v33 晋升后。 -expect_ok "validate_skill_evolution with SKIP bypasses step 8" \ - env SKIP_SNAPSHOT_CONSISTENCY=1 SKIP_BEHAVIOR_VALIDATION=1 bash scripts/validate_skill_evolution.sh - -# ---- scripts/*.sh must all be executable ---- -# 防止未来脚本因复制 / 重建丢失 +x 位导致 evolution 工作流静默损坏。 -missing_exec_count=0 -for s in scripts/*.sh; do - if [ ! -x "$s" ]; then - if [ "$missing_exec_count" -eq 0 ]; then - echo "FAIL: scripts/*.sh missing +x:" - fi - echo " - $s" - missing_exec_count=$((missing_exec_count+1)) - fi -done -if [ "$missing_exec_count" -eq 0 ]; then - pass=$((pass+1)) -else - fail=$((fail+1)) -fi - -echo "---" -echo "Passed: ${pass}" -echo "Failed: ${fail}" -if [ "$fail" -ne 0 ]; then - exit 1 -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/update_skill_proposal_status.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/update_skill_proposal_status.sh deleted file mode 100755 index a24fbfd..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/update_skill_proposal_status.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/update_skill_proposal_status.sh " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_rule_ids.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_rule_ids.sh deleted file mode 100755 index ee8fbea..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_rule_ids.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -INDEX_FILE="references/rule_index.md" -SKILL_FILE="SKILL.md" - -if [ ! -f "$INDEX_FILE" ]; then - echo "Missing rule index: ${INDEX_FILE}" - exit 1 -fi - -if [ ! -f "$SKILL_FILE" ]; then - echo "Missing skill file: ${SKILL_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" - -skill_file = "SKILL.md" -index_file = "references/rule_index.md" -scenario_dir = "evolution/scenarios" - -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -ALLOWED_STATUS = %w[active retired deprecated].freeze - -violations = [] - -# ---- Parse SKILL.md inline IDs ---- -skill_ids = [] -File.foreach(skill_file).with_index(1) do |line, lineno| - line.scan(/\[([A-Z]+-\d{3})\]/).each do |match| - skill_ids << { id: match[0], line: lineno } - end -end - -skill_id_set = skill_ids.map { |e| e[:id] } -skill_id_uniq = skill_id_set.uniq - -if skill_id_set.length != skill_id_uniq.length - dupes = skill_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, occurrences| - locs = skill_ids.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{skill_file}: duplicate ID #{id} (#{locs})" - end -end - -skill_id_set = skill_id_uniq.to_set rescue skill_id_uniq - -skill_id_uniq.each do |id| - unless id =~ ID_FORMAT - violations << "#{skill_file}: ID '#{id}' violates format ^[A-Z]+-\\d{3}$" - end -end - -# ---- Parse rule_index.md table rows ---- -# Match table rows whose first cell is an ID-shaped token, second cell is status. -# Format: | ID | status | ... -index_entries = [] -File.foreach(index_file).with_index(1) do |line, lineno| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*([A-Za-z][A-Za-z0-9-]*)\s*\|/) - next unless m - index_entries << { id: m[1], status: m[2], line: lineno } -end - -if index_entries.empty? - violations << "#{index_file}: no rule rows parsed (expected '| ID | status | ... |')" -end - -index_id_set = index_entries.map { |e| e[:id] } -index_id_uniq = index_id_set.uniq - -if index_id_set.length != index_id_uniq.length - dupes = index_id_set.group_by { |id| id }.select { |_, v| v.length > 1 } - dupes.each do |id, _| - locs = index_entries.select { |e| e[:id] == id }.map { |e| "line #{e[:line]}" }.join(", ") - violations << "#{index_file}: duplicate ID #{id} (#{locs})" - end -end - -# ---- Status enum check ---- -index_entries.each do |entry| - unless ALLOWED_STATUS.include?(entry[:status]) - violations << "#{index_file}:#{entry[:line]}: status '#{entry[:status]}' not in #{ALLOWED_STATUS.inspect}" - end -end - -# ---- Bidirectional set equality ---- -skill_set = skill_id_uniq.sort -index_set = index_id_uniq.sort - -missing_in_index = skill_set - index_set -missing_in_skill = index_set - skill_set - -missing_in_index.each do |id| - violations << "Mismatch: SKILL.md has '#{id}' but rule_index.md does not" -end - -missing_in_skill.each do |id| - status = index_entries.find { |e| e[:id] == id }&.dig(:status) - if status == "retired" || status == "deprecated" - # Retired IDs are expected to be absent from SKILL.md — skip. - next - end - violations << "Mismatch: rule_index.md has '#{id}' (status=#{status || 'unknown'}) but SKILL.md does not" -end - -# ---- Retired IDs must NOT appear in SKILL.md ---- -retired_ids = index_entries.select { |e| %w[retired deprecated].include?(e[:status]) }.map { |e| e[:id] } -retired_ids.each do |id| - if skill_id_uniq.include?(id) - violations << "Retired/deprecated ID '#{id}' still present in SKILL.md — remove inline reference" - end -end - -# ---- Scenario rule_id references ---- -active_ids = index_entries.select { |e| e[:status] == "active" }.map { |e| e[:id] }.to_set -all_index_ids = index_id_uniq.to_set - -if Dir.exist?(scenario_dir) - Dir.glob(File.join(scenario_dir, "*.json")).sort.each do |file| - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError - # Spec validator handles parse errors; skip here. - next - end - - %w[expected_hits failure_signals].each do |section| - entries = data[section] - next unless entries.is_a?(Array) - entries.each_with_index do |entry, idx| - next unless entry.is_a?(Hash) && entry.key?("rule_id") - rid = entry["rule_id"] - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid.inspect}' violates format" - next - end - unless all_index_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' not found in rule_index.md" - next - end - unless active_ids.include?(rid) - violations << "#{file}: #{section}[#{idx}].rule_id '#{rid}' references retired/deprecated rule" - end - end - end - end -end - -if violations.empty? - puts "Rule IDs OK (#{skill_id_uniq.length} IDs in SKILL.md, #{index_id_uniq.length} in rule_index.md, #{active_ids.length} active)" - exit 0 -else - puts "Rule ID validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_scenario_specs.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_scenario_specs.sh deleted file mode 100755 index 696f825..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_scenario_specs.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -SCENARIO_DIR="evolution/scenarios" - -if [ ! -d "$SCENARIO_DIR" ]; then - echo "Missing scenarios directory: ${SCENARIO_DIR}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -scenario_dir = "evolution/scenarios" - -# Canonical slug set; mirrors references/validation_scenarios.md "建议使用固定场景标识". -CANONICAL_SLUGS = %w[ - layout - parameter-pass-through - concurrency - review - migration - mcp-control -].freeze - -OUTPUT_CONTRACTS = %w[four-segment findings-first free].freeze - -REQUIRED_FIELDS = %w[ - id - version - category - input - primary_refs - output_contract - expected_hits - failure_signals - scoring -].freeze - -violations = [] -seen_ids = Set.new - -files = Dir.glob(File.join(scenario_dir, "*.json")).sort - -if files.empty? - violations << "No scenario specs found under #{scenario_dir}" -end - -files.each do |file| - basename = File.basename(file, ".json") - begin - data = JSON.parse(File.read(file)) - rescue JSON::ParserError => e - violations << "#{file}: invalid JSON — #{e.message}" - next - end - - REQUIRED_FIELDS.each do |field| - unless data.key?(field) - violations << "#{file}: missing required field '#{field}'" - end - end - - id = data["id"] - if id.nil? || id.to_s.empty? - violations << "#{file}: empty id" - else - if id != basename - violations << "#{file}: id '#{id}' does not match filename '#{basename}'" - end - unless CANONICAL_SLUGS.include?(id) - violations << "#{file}: id '#{id}' not in canonical slug set #{CANONICAL_SLUGS.inspect}" - end - if seen_ids.include?(id) - violations << "#{file}: duplicate id '#{id}'" - else - seen_ids << id - end - end - - if data["version"] != 1 - violations << "#{file}: unsupported version #{data['version'].inspect} (expected 1)" - end - - input = data["input"] - if !input.is_a?(String) || input.strip.empty? - violations << "#{file}: input must be a non-empty string" - end - - primary_refs = data["primary_refs"] - if !primary_refs.is_a?(Array) || primary_refs.empty? - violations << "#{file}: primary_refs must be a non-empty array" - else - primary_refs.each do |path| - unless path.is_a?(String) && File.exist?(path) - violations << "#{file}: primary_refs entry '#{path}' does not exist" - end - end - end - - contract = data["output_contract"] - unless OUTPUT_CONTRACTS.include?(contract) - violations << "#{file}: output_contract '#{contract}' not in #{OUTPUT_CONTRACTS.inspect}" - end - - hit_keys = [] - expected_hits = data["expected_hits"] - if !expected_hits.is_a?(Array) || expected_hits.empty? - violations << "#{file}: expected_hits must be a non-empty array" - else - expected_hits.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: expected_hits[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: expected_hits[#{idx}].key '#{key}' must be lowercase kebab-case" - else - hit_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: expected_hits[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: expected_hits[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - signal_keys = [] - failure_signals = data["failure_signals"] - if !failure_signals.is_a?(Array) || failure_signals.empty? - violations << "#{file}: failure_signals must be a non-empty array" - else - failure_signals.each_with_index do |entry, idx| - unless entry.is_a?(Hash) - violations << "#{file}: failure_signals[#{idx}] must be an object" - next - end - key = entry["key"] - desc = entry["desc"] - if !key.is_a?(String) || key !~ /\A[a-z0-9][a-z0-9-]*\z/ - violations << "#{file}: failure_signals[#{idx}].key '#{key}' must be lowercase kebab-case" - else - signal_keys << key - end - if !desc.is_a?(String) || desc.strip.empty? - violations << "#{file}: failure_signals[#{idx}].desc must be a non-empty string" - end - if entry.key?("rule_id") - rid = entry["rule_id"] - if !rid.is_a?(String) || rid !~ /\A[A-Z]+-\d{3}\z/ - violations << "#{file}: failure_signals[#{idx}].rule_id '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - end - end - end - end - - combined = hit_keys + signal_keys - if combined.uniq.length != combined.length - dupes = combined.group_by { |k| k }.select { |_, v| v.length > 1 }.keys - violations << "#{file}: duplicate key(s) across expected_hits and failure_signals: #{dupes.join(', ')}" - end - - scoring = data["scoring"] - if !scoring.is_a?(Hash) - violations << "#{file}: scoring must be an object" - else - %w[pass partial fail].each do |bucket| - unless scoring[bucket].is_a?(String) && !scoring[bucket].strip.empty? - violations << "#{file}: scoring.#{bucket} must be a non-empty string" - end - end - end -end - -missing_slugs = CANONICAL_SLUGS - seen_ids.to_a -unless missing_slugs.empty? - violations << "Missing scenario specs for canonical slugs: #{missing_slugs.join(', ')}" -end - -if violations.empty? - puts "Scenario specs OK (#{files.length} files, #{seen_ids.length} canonical slugs covered)" - exit 0 -else - puts "Scenario spec validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index 6ed9739..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/13] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/13] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/13] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/13] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" - -echo "[5/13] Validate internal markdown links" -ruby <<'RUBY' -broken = 0 -Dir.glob('references/*.md').sort.each do |file| - File.foreach(file).with_index(1) do |line, lineno| - line.scan(/\[([^\]]*)\]\(([^)]+)\)/) do |_text, link| - next if link =~ /\A(https?|mailto):/i - path = link.split('#', 2).first.to_s - next if path.empty? - full = File.expand_path(path, File.dirname(file)) - unless File.exist?(full) - puts "Broken link in #{file}:#{lineno} -> #{link} (resolved: #{full})" - broken += 1 - end - end - end -end -exit 1 if broken > 0 -RUBY -echo "Internal links OK" - -echo "[6/13] Validate scenario specs" -bash scripts/validate_scenario_specs.sh - -echo "[7/13] Validate rule IDs" -bash scripts/validate_rule_ids.sh - -echo "[8/13] Validate usage ledger" -bash scripts/validate_usage_ledger.sh - -echo "[9/13] Validate no orphan references" -ruby <<'RUBY' -referenced = {} -# SKILL.md 直接引用 -File.read('SKILL.md').scan(/references\/([A-Za-z0-9_.-]+\.md)/).each do |match| - referenced[match[0]] = true -end -# references 内部互引 -Dir.glob('references/*.md').each do |file| - File.read(file).scan(/\(([A-Za-z0-9_.-]+\.md)(?:#[^)]*)?\)/).each do |match| - referenced[match[0]] = true - end -end - -orphans = [] -Dir.glob('references/*.md').sort.each do |file| - name = File.basename(file) - orphans << file unless referenced[name] -end - -unless orphans.empty? - puts "Orphan references (not referenced by SKILL.md or any other ref):" - orphans.each { |f| puts " #{f}" } - exit 1 -end -RUBY -echo "No orphan references" - -echo "[10/13] Validate unique ownership + retired word regression" -ruby <<'RUBY' -# pattern => [expected_owner_basename, description] -UNIQUE_OWNERS = { - /传输错误.*状态码错误.*解码错误.*鉴权错误.*业务错误.*展示错误/m => ['domain_modeling.md', '错误分层 6 层枚举'], - /Time Profiler[^\n]{0,30}[::][^\n]*定位[^\n]*CPU/m => ['observability_logging.md', '完整性能取证工具用途定义(Time Profiler: 定位 CPU)'], - /审查结论[\s\S]{0,300}?严重问题[\s\S]{0,300}?一般问题[\s\S]{0,300}?验证缺口[\s\S]{0,300}?最终要求/m => ['review_checklists.md', 'findings-first 五段标签完整定义'], -} - -# 退役词:模式 => 说明 -RETIRED_TERMS = { - /错误[^\n]{0,30}协议层|协议层[^\n]{0,30}错误/m => '"协议层" 作为错误分层名已退役(Issue D2),改用 "状态码错误"', -} - -violations = 0 - -files_to_check = ['SKILL.md'] + Dir.glob('references/*.md').sort - -UNIQUE_OWNERS.each do |pattern, (owner, desc)| - files_to_check.each do |file| - next if File.basename(file) == owner - content = File.read(file) - if content =~ pattern - puts "Unique ownership violated: #{desc} (应只在 #{owner}) 却在 #{file} 出现" - violations += 1 - end - end -end - -RETIRED_TERMS.each do |pattern, desc| - files_to_check.each do |file| - content = File.read(file) - if content =~ pattern - puts "Retired term regression in #{file}: #{desc}" - violations += 1 - end - end -end - -exit 1 if violations > 0 -RUBY -echo "Unique ownership + retired words OK" - -echo "[11/13] Validate threshold doc/script sync" -ruby <<'RUBY' -script_path = "scripts/summarize_usage_ledger.sh" -doc_path = "references/usage_ledger.md" - -script_consts = {} -File.foreach(script_path) do |line| - m = line.match(/^([A-Z_]+_THRESHOLD)\s*=\s*([0-9.]+)\s*$/) - next unless m - script_consts[m[1]] = m[2] -end - -required = %w[MISSED_RULE_THRESHOLD TASK_TYPE_OTHER_THRESHOLD DEVIATION_THRESHOLD TOOL_DIVERGENCE_THRESHOLD] -missing = required - script_consts.keys -unless missing.empty? - puts "Missing threshold constants in #{script_path}: #{missing.join(', ')}" - exit 1 -end - -doc_consts = {} -File.foreach(doc_path) do |line| - m = line.match(/^\|\s*`([A-Z_]+_THRESHOLD)`\s*\|\s*([0-9.]+)\s*\|/) - next unless m - doc_consts[m[1]] = m[2] -end - -doc_missing = required - doc_consts.keys -unless doc_missing.empty? - puts "Missing threshold rows in #{doc_path} §8: #{doc_missing.join(', ')}" - exit 1 -end - -drift = [] -required.each do |k| - if script_consts[k] != doc_consts[k] - drift << "#{k}: script=#{script_consts[k]} doc=#{doc_consts[k]}" - end -end -unless drift.empty? - puts "Threshold drift between #{script_path} and #{doc_path} §8:" - drift.each { |d| puts " #{d}" } - exit 1 -end -RUBY -echo "Threshold doc/script sync OK" - -echo "[12/13] Validate snapshot consistency with active version" -if [ "${SKIP_SNAPSHOT_CONSISTENCY:-0}" = "1" ]; then - echo "Skipped (SKIP_SNAPSHOT_CONSISTENCY=1)" -else - bash scripts/check_snapshot_consistency.sh -fi - -echo "[13/13] Run behavior validation scenarios" -if [ "${SKIP_BEHAVIOR_VALIDATION:-0}" = "1" ]; then - echo "Skipped (SKIP_BEHAVIOR_VALIDATION=1)" -else - SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/run_behavior_validation.sh -fi - -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100755 index 16f0ba6..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -# 字段白名单校验 -if [[ ! "$proposal_file" =~ ^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$ ]]; then - echo "Invalid proposal_file format: ${proposal_file}" - exit 1 -fi - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -for slug in "$@"; do - if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]{0,50}$ ]]; then - echo "Invalid scenario slug format: ${slug}" - exit 1 - fi -done - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -SKIP_SNAPSHOT_CONSISTENCY=1 bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -active_version="$(ruby -rjson -e 'print JSON.parse(File.read("evolution/active_version.json"))["active_version"]')" - -# 用 ruby JSON.pretty_generate 安全写入全部字段 -ruby -rjson - "$proposal_id" "$proposal_file" "$timestamp" "$status" "$exit_code" "$active_version" "$tmp_output" "$record_file" "$@" <<'RUBY' -proposal_id, proposal_file, timestamp, status, exit_code, active_version, tmp_output_path, record_file, *slugs = ARGV - -scenario_records = slugs.reject(&:empty?).map do |slug| - { - "scenario" => slug, - "result" => "pending", - "hits" => [], - "deviations" => [], - "improvements" => [] - } -end - -scenario_status = scenario_records.empty? ? "not_run" : "pending" -base_validation_output = File.read(tmp_output_path) - -data = { - "proposal_id" => proposal_id, - "proposal_file" => proposal_file, - "validated_at" => timestamp, - "status" => status, - "exit_code" => exit_code.to_i, - "active_version" => active_version, - "base_validation_output" => base_validation_output, - "promotion_readiness" => "not_ready", - "scenario_validation_status" => scenario_status, - "scenario_records" => scenario_records -} - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -rm -f "$tmp_output" - -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$status" >/dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_usage_ledger.sh b/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_usage_ledger.sh deleted file mode 100755 index 72591a8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v63/snapshot/scripts/validate_usage_ledger.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -LEDGER_FILE="evolution/usage/usage.jsonl" -RULE_INDEX_FILE="references/rule_index.md" - -if [ ! -f "$LEDGER_FILE" ]; then - echo "Usage ledger missing (treated as empty): ${LEDGER_FILE}" - exit 0 -fi - -if [ ! -f "$RULE_INDEX_FILE" ]; then - echo "Missing rule index: ${RULE_INDEX_FILE}" - exit 1 -fi - -ruby <<'RUBY' -require "json" -require "set" - -ledger_path = "evolution/usage/usage.jsonl" -index_path = "references/rule_index.md" - -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze -ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -TIME_FORMAT = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)\z/ - -REQUIRED_FIELDS = %w[ - time tool session_id prompt_summary task_type - expected_rules hit_rules missed_rules deviations - outcome evolution_signal -].freeze - -active_ids = Set.new -File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m -end - -violations = [] -total_lines = 0 - -File.foreach(ledger_path).with_index(1) do |raw, lineno| - raw = raw.strip - next if raw.empty? - total_lines += 1 - - begin - entry = JSON.parse(raw) - rescue JSON::ParserError => e - violations << "line #{lineno}: invalid JSON — #{e.message}" - next - end - - unless entry.is_a?(Hash) - violations << "line #{lineno}: must be a JSON object" - next - end - - REQUIRED_FIELDS.each do |f| - unless entry.key?(f) - violations << "line #{lineno}: missing required field '#{f}'" - end - end - next if REQUIRED_FIELDS.any? { |f| !entry.key?(f) } - - # time - unless entry["time"].is_a?(String) && entry["time"] =~ TIME_FORMAT - violations << "line #{lineno}: time '#{entry['time']}' must match ISO8601 with TZ" - end - - # tool - unless ALLOWED_TOOLS.include?(entry["tool"]) - violations << "line #{lineno}: tool '#{entry['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" - end - - # session_id - unless entry["session_id"].nil? || entry["session_id"].is_a?(String) - violations << "line #{lineno}: session_id must be string or null" - end - - # prompt_summary - ps = entry["prompt_summary"] - if !ps.is_a?(String) - violations << "line #{lineno}: prompt_summary must be a string" - elsif !ps.length.between?(5, 200) - violations << "line #{lineno}: prompt_summary length must be 5-200 chars (got #{ps.length})" - end - - # task_type - unless ALLOWED_TASK_TYPES.include?(entry["task_type"]) - violations << "line #{lineno}: task_type '#{entry['task_type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" - end - - # expected_rules / hit_rules — arrays of active rule_ids - %w[expected_rules hit_rules].each do |field| - arr = entry[field] - unless arr.is_a?(Array) - violations << "line #{lineno}: #{field} must be an array" - next - end - arr.each do |rid| - unless rid.is_a?(String) && rid =~ ID_FORMAT - violations << "line #{lineno}: #{field} entry '#{rid.inspect}' must match ^[A-Z]+-\\d{3}$" - next - end - unless active_ids.include?(rid) - violations << "line #{lineno}: #{field} entry '#{rid}' not in rule_index.md active set" - end - end - end - - # missed_rules consistency - expected = entry["expected_rules"] - hit = entry["hit_rules"] - missed = entry["missed_rules"] - if expected.is_a?(Array) && hit.is_a?(Array) && missed.is_a?(Array) - expected_diff = expected.reject { |r| hit.include?(r) } - if missed.sort != expected_diff.sort - violations << "line #{lineno}: missed_rules #{missed.inspect} != expected_rules - hit_rules #{expected_diff.inspect}" - end - end - - # deviations - deviations = entry["deviations"] - unless deviations.is_a?(Array) && deviations.all? { |d| d.is_a?(String) } - violations << "line #{lineno}: deviations must be array of strings" - end - - # outcome - unless ALLOWED_OUTCOMES.include?(entry["outcome"]) - violations << "line #{lineno}: outcome '#{entry['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" - end - - # evolution_signal - unless ALLOWED_SIGNALS.include?(entry["evolution_signal"]) - violations << "line #{lineno}: evolution_signal '#{entry['evolution_signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" - end -end - -if violations.empty? - puts "Usage ledger OK (#{total_lines} entries, #{active_ids.length} active rule IDs)" - exit 0 -else - puts "Usage ledger validation failed:" - violations.each { |v| puts " - #{v}" } - exit 1 -end -RUBY diff --git a/skills-engineering/ios-engineer/evolution/history/v7/metadata.json b/skills-engineering/ios-engineer/evolution/history/v7/metadata.json deleted file mode 100644 index b4d61b2..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v7", - "promoted_at": "2026-04-30T10:24:33+0800", - "source": "proposal:20260430-102256-retire-ui-layout-discipline" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/SKILL.md deleted file mode 100644 index ac42f6f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 核心职责 -- 以资深 iOS 工程师和架构师视角处理生产环境问题,优先保证正确性、可维护性、可测试性和可观测性。 -- 先确认边界、数据流、并发隔离、生命周期和验证路径,再给方案或代码。 -- 先读最少必要的代码和参考资料,不一次性加载全部 `references/`。 - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 不复述已确认上下文,不输出教科书式背景,不为展示思考过程而扩写无关分析。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 -- 需要构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 首步分流 -先把任务归入一个主类,默认只读取该主类对应的 2 到 4 份文档;若命中高风险门禁再追加附加文档。当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 强制纪律 -- 严格执行分层边界、依赖注入、单向数据流和模块治理。 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v7/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v8/metadata.json b/skills-engineering/ios-engineer/evolution/history/v8/metadata.json deleted file mode 100644 index 3a0eafb..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v8", - "promoted_at": "2026-04-30T10:37:57+0800", - "source": "proposal:20260430-103514-retire-decorative-slogans" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/SKILL.md deleted file mode 100644 index 3e45827..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/SKILL.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: ios-engineer -description: Production-grade iOS engineering skill for Swift, SwiftUI, UIKit, modular architecture, state modeling, Swift 6 concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing, and release risk control. Use when Codex needs to analyze or implement changes in an iOS codebase, review PRs, design modules, debug crashes or layout/concurrency/performance issues, plan migrations, or produce production-ready Swift code. Respond in Simplified Chinese. ---- - -# iOS Engineer - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 -- 需要构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 首步分流 -先把任务归入一个主类,默认只读取该主类对应的 2 到 4 份文档;若命中高风险门禁再追加附加文档。当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 强制纪律 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v8/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/evolution/history/v9/metadata.json b/skills-engineering/ios-engineer/evolution/history/v9/metadata.json deleted file mode 100644 index ccffad8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "v9", - "promoted_at": "2026-04-30T10:39:22+0800", - "source": "proposal:20260430-103808-slim-frontmatter-description" -} diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/SKILL.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/SKILL.md deleted file mode 100644 index d750610..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/SKILL.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: ios-engineer -description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. ---- - -# iOS Engineer - -## 规则分层 -### 1. 核心铁律 -- 始终使用简体中文。 -- 对描述不清、上下文不足或存在歧义的问题,先确认关键事实,不自行猜测需求、边界或期望行为。 -- 默认先锁定 1 个最高概率根因或主路径,最多补充 1 个备选;不要同时展开多个大分支。 -- 默认按“根因 -> 为什么 -> 修法 -> 验证”输出;若任务命中长模板要求,四段式作为摘要层,详细模板作为附加层。 -- 先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构。 -- 统一遵守 [terminology.md](references/terminology.md)。 - -### 2. 场景规则 -- 涉及架构边界、状态归属、网络链路、参数透传时,遵守 [architecture_and_network.md](references/architecture_and_network.md)。 -- 涉及页面状态、列表状态、表单状态、异步回写时,遵守 [ui_state_patterns.md](references/ui_state_patterns.md) 和 [domain_modeling.md](references/domain_modeling.md)。 -- 涉及并发设计、取消链路、过期结果回写、旧接口桥接时,遵守 [swift_concurrency.md](references/swift_concurrency.md)。 -- 涉及 Auto Layout、SwiftUI 稳定性、列表复用、无障碍时,遵守 [layout_and_ui.md](references/layout_and_ui.md)。 -- 涉及根因排查、偶现问题、补丁式修复风险时,遵守 [root_cause_enforcement.md](references/root_cause_enforcement.md)。 -- 涉及工具预算、搜索、日志取证、多轮排查时,遵守 [mcp_control.md](references/mcp_control.md)。 -- 涉及代码审查时,遵守 [review_checklists.md](references/review_checklists.md)。 -- 涉及重构、迁移、发布、灰度、回滚时,遵守 [migration_strategy.md](references/migration_strategy.md) 和 [build_release_and_ci.md](references/build_release_and_ci.md)。 -- 涉及分页、缓存、重试、鉴权、上传下载、幂等去重等具体网络模式时,遵守 [networking_patterns.md](references/networking_patterns.md)。 -- 涉及日志分层、必记字段、性能观测、排障取证时,遵守 [observability_logging.md](references/observability_logging.md)。 -- 涉及启动、列表卡顿、SwiftUI 过度刷新、内存治理、性能基线时,遵守 [performance_optimization.md](references/performance_optimization.md)。 -- 涉及接手遗留页、排查偶现 Crash、性能优化、并发迁移、大型重构等复杂任务时,先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本。 -- 涉及跨模块协作、PR 拆分、ownership、技术债记录时,遵守 [team_collaboration.md](references/team_collaboration.md)。 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格约束时,遵守 [swift_style.md](references/swift_style.md)。 -- 涉及 skill 本身的规则缺失、规则冲突、规则退役、自进化治理时,遵守 [self_evolution.md](references/self_evolution.md)。 - -### 3. 输出模板 -- 需要正式输出时,读取 [examples.md](references/examples.md)。 -- 需要产线骨架时,读取 [code_templates.md](references/code_templates.md)。 -- 需要测试与验证范围时,读取 [testing_strategy.md](references/testing_strategy.md)。 -- 需要架构裁决时,读取 [decision_records.md](references/decision_records.md)。 -- 需要构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,读取 [test_system_prompt.md](references/test_system_prompt.md),并结合 [testing_strategy.md](references/testing_strategy.md) 执行。 - -## 首步分流 -先把任务归入一个主类,默认只读取该主类对应的 2 到 4 份文档;若命中高风险门禁再追加附加文档。当任务跨越多个维度时,优先顺序是:根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险。 - -- 排障: - 读取 [root_cause_enforcement.md](references/root_cause_enforcement.md),再按问题性质追加并发、布局、状态、网络或 [observability_logging.md](references/observability_logging.md) 文档。 -- 设计与实现: - 读取 [architecture_and_network.md](references/architecture_and_network.md)、[domain_modeling.md](references/domain_modeling.md)、[ui_state_patterns.md](references/ui_state_patterns.md) 中最相关的文档;涉及具体网络模式时追加 [networking_patterns.md](references/networking_patterns.md)。 -- 代码审查: - 读取 [review_checklists.md](references/review_checklists.md),必要时追加 [anti_patterns.md](references/anti_patterns.md) 或 [team_collaboration.md](references/team_collaboration.md)。 -- 迁移与发布: - 读取 [migration_strategy.md](references/migration_strategy.md),必要时追加 [build_release_and_ci.md](references/build_release_and_ci.md)、[decision_records.md](references/decision_records.md)。 -- 性能优化: - 读取 [performance_optimization.md](references/performance_optimization.md),必要时追加 [observability_logging.md](references/observability_logging.md) 和 [swift_concurrency.md](references/swift_concurrency.md)。 -- 复杂任务剧本: - 读取 [execution_playbooks.md](references/execution_playbooks.md),再按所选剧本补充对应主文档。 -- Skill 验证: - 读取 [validation_scenarios.md](references/validation_scenarios.md)。 -- Skill 维护与自进化: - 读取 [self_evolution.md](references/self_evolution.md),必要时追加 [validation_scenarios.md](references/validation_scenarios.md) 和 [testing_strategy.md](references/testing_strategy.md)。 - -## 强制纪律 -- 不要格式化代码,除非明确要求格式化当前代码。 -- 任何新增或修改规则,必须说明它是在新增能力、修正表达、合并重复,还是退役旧规则;若不能说明替代关系,默认不新增。 - -## 交付门禁 -- 任何改动都必须声明“已覆盖、未覆盖、残留风险”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/agents/openai.yaml b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/agents/openai.yaml deleted file mode 100644 index 4e5f5f4..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "iOS Engineer" - short_description: "生产级 iOS 工程与架构技能,覆盖设计、实现、排障、Review、迁移与发布治理。" - default_prompt: "Use $ios-engineer to handle production-grade iOS work in Simplified Chinese. If the request is unstructured, first normalize it as symptom, known facts, most likely root cause, minimal fix, and verification. Prefer the most likely root cause first, keep context tight, avoid loops, and default to root cause, why, fix, and verify unless the user asks for more." diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/anti_patterns.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/anti_patterns.md deleted file mode 100644 index a423a0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/anti_patterns.md +++ /dev/null @@ -1,202 +0,0 @@ -# iOS 反模式库 - -## 目录 -- 使用规则 -- 架构反模式 -- 并发反模式 -- UI 与状态反模式 -- 网络与数据反模式 -- 性能反模式 -- 排障反模式 - -## 使用规则 -- 发现以下反模式时,必须直接指出,不得淡化为“个人风格差异”。 -- 识别到反模式后,必须说明它破坏了哪一层边界、会引发什么风险、应该如何重构。 - -## 1. 架构反模式 -### Massive ViewController / Massive ViewModel -表现: -- 控制器或 ViewModel 同时负责渲染、路由、网络、缓存、埋点、权限和状态拼装。 - -风险: -- 不可测试 -- 难以复用 -- 改一处牵一片 - -修法: -- 拆出 UseCase、Repository、Coordinator、DataSource、Service。 - -### 伪模块化 -表现: -- 拆了多个目录或 Package,但依赖方向混乱,任何模块都能直接访问任何实现。 - -风险: -- 模块边界失效 -- 无法独立演进 - -修法: -- 收敛公开 API,修正依赖方向,禁止跨模块直连内部实现。 - -### 万能 Manager -表现: -- 一个 `Manager` 同时承担网络、缓存、状态同步和业务决策。 - -风险: -- 单点膨胀 -- 责任失控 - -修法: -- 拆职责,保留抽象接口,按通信、存储、状态、业务规则分层。 - -## 2. 并发反模式 -### 散落式 `Task {}` -表现: -- 在 View、Cell、回调、工具类中到处直接起任务,没有归属和取消关系。 - -风险: -- 取消失效 -- 状态回写错位 -- 生命周期泄漏 - -修法: -- 收拢到结构化并发,建立父子任务关系。 - -### `DispatchQueue.main.async` 掩盖时序问题 -表现: -- 一出 UI 或状态问题就往主线程异步包一层。 - -风险: -- 问题被延后,不是被修复 -- 产生新的竞态窗口 - -修法: -- 明确隔离域、状态源和回写时机。 - -### 滥用 `@unchecked Sendable` -表现: -- 为了消除编译警告,直接给引用类型打 `@unchecked Sendable`。 - -风险: -- 把真实数据竞争伪装成“已处理” - -修法: -- 改值语义、actor 化或增加严格同步保护,并写清理由。 - -## 3. UI 与状态反模式 -### 状态源散落 -表现: -- 同一份页面状态在 View、ViewModel、Service、缓存层各维护一份。 - -风险: -- 状态不一致 -- 列表错位 -- 表单回填异常 - -修法: -- 定义单一真相源,统一状态流和写入路径。 - -### 写死尺寸修布局 -表现: -- 通过固定宽高、额外空白、魔法间距修页面。 - -风险: -- 多语言、极端字号、横竖屏全部失效 - -修法: -- 回到约束关系、内容自适应和布局语义本身。 - -### 不稳定的列表身份 -表现: -- `id` 不稳定,或用 index 充当长期身份。 - -风险: -- 滚动位置丢失 -- 动画错乱 -- 复用状态串位 - -修法: -- 使用稳定业务标识作为身份。 - -## 4. 网络与数据反模式 -### 字符串拼装请求 -表现: -- URL、Header、Query、Body 到处手写。 - -风险: -- 不一致 -- 不可测试 -- 难以审计 - -修法: -- 统一 Endpoint 和 Request 构建层。 - -### 错误透传到 UI -表现: -- 直接把底层 `Error.localizedDescription` 展示给用户。 - -风险: -- 语义错误 -- 用户体验差 -- 错误边界失控 - -修法: -- 建立错误分层和面向 UI 的错误映射。 - -### 盲目重试 -表现: -- 失败就自动重试,不区分幂等和业务语义。 - -风险: -- 重复下单 -- 重复提交 -- 服务端雪崩 - -修法: -- 只对允许重试的请求定义有限次、可追踪的重试策略。 - -## 5. 性能反模式 -### 主线程做重活 -表现: -- 主线程做图片解码、富文本解析、复杂排序、同步 IO。 - -风险: -- 掉帧 -- 首屏慢 -- 手势阻塞 - -修法: -- 下沉非 UI 工作,控制回切时机。 - -### 为了性能牺牲正确性 -表现: -- 通过缓存脏状态、跳过刷新、吞异常换取“更快”。 - -风险: -- 数据错误 -- UI 不一致 - -修法: -- 先保证正确性,再基于指标优化实现。 - -## 6. 排障反模式 -### 现象即根因 -表现: -- 把报错点、崩溃栈最后一帧、页面异常位置直接当根因。 - -风险: -- 修错位置 -- 问题反复出现 - -修法: -- 按完整链路回溯到数据、状态、并发和生命周期源头。 - -### 补丁式修复 -表现: -- 增加 `if`、延迟、重载、兜底分支压住问题。 - -风险: -- 隐性问题堆积 -- 下次更难排查 - -修法: -- 做结构性修复,并补验证证据。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/architecture_and_network.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/architecture_and_network.md deleted file mode 100644 index 4c5e89f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/architecture_and_network.md +++ /dev/null @@ -1,116 +0,0 @@ -# 架构与网络层设计 - -## 适用场景 -用于以下任务: -- 设计新模块、业务域拆分、依赖治理 -- 设计 Repository / Service / UseCase / Coordinator -- 规划网络层、缓存层、鉴权、重试与错误处理 -- 审查 Controller 膨胀、耦合失控、边界不清的问题 -- 用户对"当前架构"提出咨询、评估、演进建议请求 - -## 当前架构咨询 -- 当用户询问"当前架构"时,必须基于项目现有架构、真实代码组织、依赖方向、状态流和边界划分给出有价值的分析;允许直接采用"代码审查(Code Review)"级别的严格标准指出结构性问题、脆弱点和演进风险,不做保守性淡化。 -- 当用户询问"当前架构"但信息不完整时,必须先明确提出完成判断所需的补充信息,而不是直接基于猜测补全上下文或假设缺失前提。 -- 分流边界(解决"最小修复 vs 激进指出"的表面冲突): - - **架构评估 / 咨询输出**模式:用户问"当前架构""有没有问题""演进方向""是否合理"等评估类问题时,按本节第 1 条激进指出结构性问题,不因担心越界而淡化。 - - **实施代码改动**模式:用户要求"改这个方法""修这个 Bug""加这个字段"等具体改动时,遵守 SKILL.md 核心铁律"先给最小可验证修复,不先提出整模块重写、架构翻新或大范围重构";架构级建议只作为残留风险或后续方向提及,不混入本次改动。 - - 当任务混合两种模式(例如"修这个 Bug 顺便看一下架构")时,必须先完成最小修复闭环,再以独立段落输出架构评估,不把架构建议与修法捆绑。 - -## 架构强制原则 -### 分层职责 -- `ViewController` / `SwiftUI View`:只负责渲染、用户输入转发和路由触发。 -- `ViewModel` / `Presenter`:负责界面状态编排,不直接持有 UIKit / SwiftUI 视图对象。 -- `UseCase` / `Interactor`:承载业务规则和用例编排。 -- `Repository`:聚合远端、本地缓存和持久化访问。 -- `Service` / `APIClient`:只关心请求发送、解码和底层通信。 - -### 依赖方向 -- UI 层依赖业务抽象,不反向依赖具体实现。 -- 高层模块不得导入低层实现细节。 -- 通过构造器注入依赖;容器注入只用于装配,不用于隐藏依赖。 - -### 参数透传与数据来源 -- 新增字段、方法参数、构造参数或状态值时,先确认它的真实来源属于哪一层,不得默认由中间层“顺手补一个变量”。 -- 若某个值需要从上游对象透传到下游消费端,必须沿调用链补齐:数据源 -> 映射层 -> 构造点 -> 持有者 -> 使用点。 -- 动手修改前,先明确指出链路断点发生在哪一跳:谁本应创建、谁本应持有、谁当前没有继续透传。 -- 不得只在末端类里加属性、在中间类里补同名参数或临时传空值让局部编译通过。 -- 若透传链路跨越多个模块或层次,必须同时检查命名语义、可空性、默认值策略和测试覆盖是否仍然成立。 -- 若发现当前层拿不到这个值,优先回溯真实拥有者和创建点,再决定是透传、重建边界还是重构依赖。 - -### 模块化原则 -- 按 `Feature` + `Core` 组织,禁止按 `Utils`、`Manager`、`Base` 堆积。 -- SPM 模块边界要清楚定义公开 API,避免过度 `public`。 -- 不允许“跨模块直接访问内部实现”式偷渡。 - -## 典型目录规范 -```text -App -Features/ -Core/ -SharedUI/ -Infrastructure/ -``` - -约束: -- `Features` 之间通过协议或路由能力协作。 -- `Core` 放稳定抽象和通用能力,不放具体业务。 -- `Infrastructure` 放网络、数据库、日志、埋点等实现细节。 - -## 架构选型规则 -### UIKit 项目 -- 中大型项目使用 `MVVM + Coordinator` 或 `Clean Architecture`。 -- 当页面状态复杂、业务编排多、测试要求高时,引入 `UseCase` 和 `Repository`。 - -### SwiftUI 项目 -- 使用状态驱动设计,严格控制状态源数量。 -- 避免把导航、副作用、网络请求直接塞进 View。 -- 对复杂业务页,保留 ViewModel / UseCase 分层,禁止把业务逻辑塞进 `body` 附近。 - -## 网络层设计 -### 基础结构 -推荐链路: -`Endpoint -> RequestBuilder -> APIClient -> Decoder -> Repository -> UseCase -> ViewModel` - -### 强制要求 -- 统一请求抽象,禁止分散手写 URL、Header、Query。 -- 底层使用 `URLSession + async/await`。 -- 解码策略集中配置,例如日期格式、key 转换、空值兼容。 -- 错误必须分层建模:传输层、协议层、鉴权层、业务层、解码层。 -- 日志必须记录请求标识、耗时、状态码、关键上下文,但不能泄露敏感信息。 - -### 重试与超时 -- 只对幂等请求定义自动重试。 -- 重试策略必须说明触发条件、次数、退避策略和停止条件。 -- 超时必须根据业务场景分级,不允许全局一个值拍脑袋覆盖。 - -### 缓存策略 -- 先区分“展示缓存”、“业务缓存”、“离线缓存”。 -- 必须明确缓存键、失效条件、写入时机和一致性策略。 -- 不允许让 ViewModel 直接感知缓存实现细节。 - -> 详细的请求链路、分页、重试、缓存、鉴权刷新、上传下载、幂等去重模式见 [networking_patterns.md](networking_patterns.md)。 - -## 鉴权与安全 -- Token 刷新流程必须串行化,避免并发刷新风暴。 -- 认证信息存储使用 Keychain。 -- 敏感日志脱敏,避免打印完整 Token、手机号、身份证号等。 - -## 可测试性要求 -- Repository、Service、Clock、Feature Flag、Store 均应可替换。 -- ViewModel / UseCase 的输入输出应可单测,不依赖真实网络。 -- 网络层测试至少覆盖:成功、超时、取消、解码失败、鉴权失败。 - -## 常见反模式 -- ViewController 直接发请求、解析 JSON、拼接埋点。 -- ViewModel 直接导入 UIKit / SwiftUI 并操作控件。 -- 一个 `NetworkManager` 承担所有职责。 -- 到处散落 `URL(string:)`、字符串路由和魔法 Header。 -- 无错误分层,直接把 `Error.localizedDescription` 透给 UI。 - -## 方案评审清单 -- [ ] 分层职责是否清晰,是否存在越界? -- [ ] 依赖是否面向协议,是否可替换、可 Mock? -- [ ] 模块边界是否稳定,公开 API 是否最小化? -- [ ] 网络层是否统一抽象了请求、解码、错误和日志? -- [ ] 缓存、重试、鉴权是否基于业务语义,而不是临时补丁? -- [ ] 该设计是否便于测试、扩展和排障? diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/build_release_and_ci.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/build_release_and_ci.md deleted file mode 100644 index 5d1104b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/build_release_and_ci.md +++ /dev/null @@ -1,88 +0,0 @@ -# 构建、发布与 CI 治理 - -## 目录 -- 使用规则 -- 构建配置基线 -- 依赖治理 -- CI 门禁 -- 发布与灰度 -- 失败信号与回滚 -- 常见反模式 - -## 使用规则 -- 涉及构建失败、Scheme/Configuration 混乱、SPM 依赖问题、签名配置、CI 流水线、发布门禁、灰度或回滚时,必须使用本文件。 -- 不把“本地能跑”视为可交付标准,必须同时回答“CI 能否稳定构建、发布能否可控回滚、风险能否被观测”。 -- 不在没有门禁条件、失败信号和回滚路径的情况下推进发布或高风险改造。 - -## 构建配置基线 -### Scheme 与 Build Configuration -- 明确区分 `Debug`、`Release`、必要时的 `Staging`,不要让配置语义漂移。 -- Scheme 只承载启动和调试入口,不承载业务差异逻辑。 -- 环境差异通过配置注入、构建设置或运行时配置承载,不通过散落 `#if` 拼接。 - -### Target 与模块边界 -- 共享逻辑优先抽到 SPM 模块或稳定 Target,不复制粘贴到多个 Target。 -- Target 依赖方向必须单向,避免 App Target 反向引用实现细节。 -- 第三方依赖的引入位置要固定,避免同一依赖同时存在于多个包管理体系。 - -### 构建问题排查顺序 -1. 先看失败发生在哪一层:依赖解析、编译、链接、签名、打包、测试。 -2. 再确认是否和 Scheme、Configuration、SDK、Xcode 版本或缓存相关。 -3. 再确认是不是模块边界、可见性、条件编译或资源打包问题。 -4. 最后才处理缓存清理或重新生成工程文件。 - -### 模拟器与真机构建策略 -- 优先明确失败是否与模拟器 SDK、架构、系统能力或第三方二进制依赖有关。 -- 若模拟器无法完成编译验证,必须切到真机构建继续验证,而不是直接宣告无法编译。 -- 切到真机构建后,必须记录模拟器失败原因和真机验证范围,避免把平台差异误判为代码已完全正确。 -- 若问题只在真机或只在模拟器出现,必须把它视为平台差异问题单独分析,不得混为通用构建失败。 - -## 依赖治理 -### SPM -- 锁定依赖版本策略,避免无约束漂移。 -- 共享包要明确最小平台版本和公开 API 边界。 -- 包内不要泄露 App 层依赖,避免形成反向耦合。 - -### 混合依赖管理 -- 同一项目不要长期并存多套包管理方式而没有迁移计划。 -- 若暂时必须共存,明确谁是主源、谁是过渡层、何时删除旧方案。 -- 构建失败若来自二进制依赖或脚本阶段,必须记录可复现条件和环境差异。 - -## CI 门禁 -### 最低门禁 -- 必须至少包含:编译、核心测试、静态检查或等价质量门禁。 -- 合并前门禁和发布前门禁分开定义,不能混为一个口径。 -- 对高风险模块增加专项门禁,例如并发测试、快照测试、性能回归检查。 - -### 流水线设计 -- 流水线步骤保持可定位:依赖解析、构建、测试、制品、分发分别输出结果。 -- 失败日志必须能定位到模块、Target、测试用例或脚本阶段。 -- 需要缓存时,缓存策略要可失效、可回退,不把缓存变成新的不稳定源。 - -### 环境一致性 -- 固定 Xcode 版本、SDK、关键工具版本和证书来源。 -- 本地、CI、发布机之间的构建配置差异必须可见。 -- CI 里出现、而本地不出现的问题,优先排查环境、签名、资源和脚本输入输出声明。 - -## 发布与灰度 -### 发布前必答问题 -- 发布影响哪些页面、模块、埋点、缓存、关键路径? -- 是否有特性开关、路由开关或配置开关可做灰度? -- 发布后看哪些指标判断成功或失败? - -### 灰度策略 -- 高风险改动按人群、渠道、版本或开关逐步放量。 -- 新旧链路并存时,定义一致性检查方式。 -- 灰度期间,保留快速关停或回切手段,不依赖重新发版作为唯一回滚路径。 - -## 失败信号与回滚 -- 失败信号至少包括:Crash 指标、关键业务成功率、接口错误率、卡顿或启动退化、核心埋点异常。 -- 回滚条件必须量化,不写“有问题再看”。 -- 回滚路径必须可执行:关闭开关、回切旧链路、撤回配置、回退版本各自的责任人和顺序要明确。 - -## 常见反模式 -- 把环境差异写死在代码里,而不是通过配置或构建设置管理。 -- 同一依赖同时由 SPM、Pods 或手工集成管理。 -- 发布前只验证 Happy Path,不验证升级、回滚、降级和异常路径。 -- CI 失败后直接清缓存重试,不先确认失败层级和根因。 -- 没有灰度和回滚条件就推动高风险改动上线。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/code_templates.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/code_templates.md deleted file mode 100644 index edb096f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/code_templates.md +++ /dev/null @@ -1,256 +0,0 @@ -# 产线代码模板 - -## 使用规则 -- 需要给出实现方案时,从本文件选择最接近的模板再落地到具体业务。 -- 模板只提供稳定骨架,不替代业务建模、错误语义和测试策略。 -- 使用模板时,必须同时说明哪些部分是通用骨架,哪些部分需要按业务改写。 - -## 目录 -- ViewModel 模板 -- UseCase 模板 -- Repository 模板 -- APIClient 模板 -- Coordinator 模板 -- Actor 模板 - -## ViewModel 模板 -适用于: -- UIKit MVVM -- SwiftUI 状态驱动页面 -- 列表、表单、详情页状态编排 - -```swift -import Foundation - -@MainActor -final class FeatureViewModel: ObservableObject { - @Published private(set) var viewState: ViewState = .idle - - private let useCase: FeatureUseCaseProtocol - private var loadTask: Task? - - init(useCase: FeatureUseCaseProtocol) { - self.useCase = useCase - } - - deinit { - loadTask?.cancel() - } - - func load() { - loadTask?.cancel() - loadTask = Task { [weak self] in - guard let self else { return } - self.viewState = .loading - - do { - let output = try await self.useCase.execute() - guard !Task.isCancelled else { return } - self.viewState = .loaded(output) - } catch is CancellationError { - return - } catch { - self.viewState = .failed(.from(error)) - } - } - } -} - -extension FeatureViewModel { - enum ViewState: Equatable { - case idle - case loading - case loaded(FeatureOutput) - case failed(ViewError) - } -} -``` - -要求: -- ViewModel 只编排状态,不做网络细节和持久化细节。 -- 任务必须可取消。 -- 错误必须映射为 UI 可消费的语义。 - -## UseCase 模板 -适用于: -- 业务规则聚合 -- 多数据源编排 -- 领域层输入输出建模 - -```swift -import Foundation - -protocol FeatureUseCaseProtocol { - func execute() async throws -> FeatureOutput -} - -struct FeatureUseCase: FeatureUseCaseProtocol { - private let repository: FeatureRepositoryProtocol - - init(repository: FeatureRepositoryProtocol) { - self.repository = repository - } - - func execute() async throws -> FeatureOutput { - let entity = try await repository.fetch() - return FeatureOutput(entity: entity) - } -} -``` - -要求: -- UseCase 承载业务规则,不承载 UI 逻辑。 -- 输入输出必须显式建模。 - -## Repository 模板 -适用于: -- 远端 + 本地缓存聚合 -- 解耦 Service 与业务层 - -```swift -import Foundation - -protocol FeatureRepositoryProtocol { - func fetch() async throws -> FeatureEntity -} - -struct FeatureRepository: FeatureRepositoryProtocol { - private let remote: FeatureRemoteDataSourceProtocol - private let cache: FeatureCacheProtocol - - init( - remote: FeatureRemoteDataSourceProtocol, - cache: FeatureCacheProtocol - ) { - self.remote = remote - self.cache = cache - } - - func fetch() async throws -> FeatureEntity { - if let cached = try? cache.read() { - return cached - } - - let entity = try await remote.fetch() - try? cache.write(entity) - return entity - } -} -``` - -要求: -- Repository 屏蔽数据来源差异。 -- 缓存策略必须按业务语义定义,不得静默污染状态。 - -## APIClient 模板 -适用于: -- `URLSession + async/await` -- 强类型错误建模 - -```swift -import Foundation - -protocol APIClientProtocol { - func send(_ endpoint: Endpoint) async throws -> T -} - -struct APIClient: APIClientProtocol { - private let session: URLSession - private let decoder: JSONDecoder - - init( - session: URLSession = .shared, - decoder: JSONDecoder = JSONDecoder() - ) { - self.session = session - self.decoder = decoder - } - - func send(_ endpoint: Endpoint) async throws -> T { - let request = try endpoint.makeURLRequest() - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw NetworkError.invalidResponse - } - - guard 200..<300 ~= httpResponse.statusCode else { - throw NetworkError.httpStatus(httpResponse.statusCode) - } - - do { - return try decoder.decode(T.self, from: data) - } catch { - throw NetworkError.decoding(error) - } - } -} -``` - -要求: -- 请求构建、发送、解码、错误分层必须分清。 -- 不得在 APIClient 中混入业务降级逻辑。 - -## Coordinator 模板 -适用于: -- UIKit 导航编排 -- Feature 路由解耦 - -```swift -import UIKit - -protocol Coordinator: AnyObject { - func start() -} - -final class FeatureCoordinator: Coordinator { - private let navigationController: UINavigationController - private let factory: FeatureSceneFactoryProtocol - - init( - navigationController: UINavigationController, - factory: FeatureSceneFactoryProtocol - ) { - self.navigationController = navigationController - self.factory = factory - } - - func start() { - let viewController = factory.makeFeatureScene() - navigationController.pushViewController(viewController, animated: true) - } -} -``` - -要求: -- 页面不直接拼装下一个页面。 -- Coordinator 负责路由,不承载业务计算。 - -## Actor 模板 -适用于: -- 共享可变状态隔离 -- Token 刷新、内存缓存、请求去重 - -```swift -import Foundation - -actor FeatureStore { - private var storage: Value - - init(initialValue: Value) { - self.storage = initialValue - } - - func read() -> Value { - storage - } - - func update(_ transform: (inout Value) -> Void) { - transform(&storage) - } -} -``` - -要求: -- actor 只承担隔离职责,不扩大为万能容器。 -- 需要跨域传递的数据必须保持语义清晰。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/decision_records.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/decision_records.md deleted file mode 100644 index 6867123..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/decision_records.md +++ /dev/null @@ -1,89 +0,0 @@ -# 架构决策记录 - -## 使用规则 -- 涉及架构选型、模块拆分、并发模型调整、状态模型重建、网络层改造、数据流重构时,必须输出决策记录。 -- 决策记录默认先给四段式摘要,再按需追加完整裁决文档。 -- 本文件只用于方案裁决和迁移落地,不重复定义通用答法、排障纪律或工具预算。 -- 没有候选方案对比、没有风险评估、没有回滚条件,不视为有效决策记录。 - -> 跨人决策同步、ownership 与 PR 拆分规则见 [team_collaboration.md](team_collaboration.md)。 - -## 必须记录的场景 -- 选择 `MVVM + Coordinator`、`Clean Architecture`、`TCA`、`VIPER` 等架构模型 -- 拆分 SPM 模块或调整模块依赖方向 -- 引入 `actor`、`@MainActor`、`TaskGroup` 等并发边界策略 -- 引入 Repository、缓存层、离线策略、重试策略 -- 大型页面重构、列表状态治理、导航体系重建 - -## 标准输出模板 -```text -决策标题 -- 一句话描述本次要解决的核心问题 - -背景 -- 当前系统状态 -- 已存在的问题 -- 触发本次调整的原因 - -决策目标 -- 这次必须解决什么 -- 这次明确不解决什么 - -候选方案 -1. 方案 A - - 做法 - - 优点 - - 缺点 - - 风险 -2. 方案 B - - 做法 - - 优点 - - 缺点 - - 风险 - -最终决策 -- 选择哪个方案 -- 不选择其他方案的原因 - -边界与影响 -- 影响哪些模块 -- 影响哪些调用链 -- 是否影响测试、缓存、埋点、并发模型 - -实施步骤 -1. 第一步 -2. 第二步 -3. 第三步 - -风险控制 -- 最大风险点 -- 如何灰度或分阶段落地 -- 回滚条件是什么 - -验证 -- 如何证明决策成立 -- 需要哪些测试和观测指标 -``` - -使用约束: -- 若当前任务只是给出方向建议,先输出简短结论、原因、修法、验证,再视需要补全本模板。 -- 只有当方案真的会改变边界、并发模型、状态归属或迁移路径时,才展开完整决策记录。 - -## 决策质量标准 -- 必须先定义问题,再比较方案,最后作出裁决。 -- 不允许只写“采用某模式更清晰”这类空洞结论。 -- 必须明确哪些是长期收益,哪些是短期成本。 -- 必须明确技术收益和业务代价。 - -## 常见错误 -- 把“个人偏好”写成“架构结论” -- 只给终态,不给迁移路径 -- 只说优点,不说代价 -- 只说设计,不说验证 -- 只说现在可行,不说后续可维护性 - -## 简化判断规则 -- 若方案会改变模块边界,写决策记录。 -- 若方案会改变并发边界,写决策记录。 -- 若方案会改变状态归属,写决策记录。 -- 若方案会影响多个团队或多个页面,写决策记录。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/domain_modeling.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/domain_modeling.md deleted file mode 100644 index beaa79b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/domain_modeling.md +++ /dev/null @@ -1,96 +0,0 @@ -# 领域建模 - -## 目录 -- 使用规则 -- 建模分层 -- 实体建模规则 -- DTO 建模规则 -- ViewState 建模规则 -- ErrorModel 建模规则 -- 映射规则 -- 常见反模式 - -## 使用规则 -- 涉及实体设计、状态设计、错误设计、数据转换时,必须先定义建模分层。 -- 不得把服务端返回结构直接当作领域模型或 UI 模型使用。 -- 建模必须先回答三个问题:谁负责持有、谁负责转换、谁负责消费。 - -## 建模分层 -固定分为四层: -- DTO:对应接口传输结构 -- Entity:对应业务语义结构 -- ViewState:对应界面渲染状态 -- ErrorModel:对应业务或界面错误语义 - -要求: -- DTO 不得直接泄露到 ViewModel 和 View。 -- Entity 不得携带 UIKit / SwiftUI 依赖。 -- ViewState 不得反向污染 Repository 和 Service。 -- ErrorModel 不得直接透传底层 `Error` 文本。 - -## 实体建模规则 -- Entity 表达稳定业务语义,不表达接口噪音和 UI 临时状态。 -- Entity 使用值语义,使用 `struct`。 -- Entity 字段名使用业务语言,不复制后端命名噪音。 -- Entity 必须可被测试和比较;需要时显式实现 `Equatable`。 - -适合放进 Entity 的内容: -- 用户、订单、商品、会话、权限、金额、时间区间 - -不适合放进 Entity 的内容: -- 占位文案 -- Cell 展示文案 -- 按钮是否禁用 -- API 原始分页字段 - -## DTO 建模规则 -- DTO 只负责解码和传输适配。 -- DTO 可以保留接口字段命名,但必须在边界层完成转换。 -- DTO 不承载业务方法,不参与 UI 判断。 - -适合放进 DTO 的内容: -- `page` -- `pageSize` -- `nextCursor` -- `rawStatus` -- `serverTimestamp` - -## ViewState 建模规则 -- ViewState 只表达界面渲染状态。 -- ViewState 由 ViewModel 产出,不由 Repository 直接产出。 -- ViewState 必须覆盖空态、加载态、错误态、成功态,不得只建成功态。 - -推荐形式: -- 枚举态:`idle / loading / loaded / failed` -- 组合态:列表内容、刷新状态、分页状态、提示状态 - -禁止: -- 把 ViewState 和 Entity 混成一个万能模型 -- 用多个布尔值拼接复杂状态 - -> 页面状态机、列表状态、表单状态、异步回写的完整建模规则见 [ui_state_patterns.md](ui_state_patterns.md)。 - -## ErrorModel 建模规则 -- 错误必须分层:网络错误、解码错误、鉴权错误、业务错误、展示错误。 -- 面向 UI 的错误必须可映射为标题、文案、操作动作,而不是直接显示系统错误文本。 -- ErrorModel 必须说明可恢复性和用户动作。 - -适合的设计方式: -- 领域层错误:表达业务失败语义 -- 展示层错误:表达界面展示和交互动作 - -## 映射规则 -- DTO -> Entity:发生在 Repository 或 Mapper 层 -- Entity -> ViewState:发生在 ViewModel 层 -- Error -> ErrorModel:发生在错误映射层或 ViewModel 边界 - -要求: -- 映射逻辑集中,不散落在 View、Cell、Service 多处。 -- 一个方向只做一层转换,不混合多个语义层。 - -## 常见反模式 -- 直接把 DTO 传给 View -- 把 Entity 直接改造成 CellModel 后又回传业务层 -- 用一个 `Model` 同时承担 DTO、Entity、ViewState 三种职责 -- 直接展示 `localizedDescription` -- 用多个布尔值组合复杂页面状态 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/examples.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/examples.md deleted file mode 100644 index 5e0db14..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/examples.md +++ /dev/null @@ -1,164 +0,0 @@ -# 输出模板与标准答法 - -## 目录 -- 使用规则 -- 架构设计答法 -- Bug 排查答法 -- 代码审查答法 -- Swift 并发答法 -- 性能分析答法 -- 重构与迁移路线答法 -- 严格输出要求 - -## 使用规则 -- 需要输出方案、审查结论、排障结论、迁移路线、性能分析时,直接套用本文件模板。 -- 默认优先使用四段式:结论 / 为什么 / 修法 / 验证。 -- 只有在用户明确要求展开或任务本身高风险时,才追加候选方案、阶段计划或细分检查项。 -- 若同时命中测试策略、决策记录或迁移风险控制,先给四段式摘要,再追加对应详细部分。 -- 本文件只定义输出骨架,不重复定义根因分析纪律、工具预算或停损规则。 - -## 1. 架构设计答法 -适用于:模块设计、页面重构、网络层设计、状态治理。 - -输出结构: - -```text -结论 -- 推荐采用什么结构 -- 边界和依赖方向怎么定 - -为什么 -- 当前核心问题是什么 -- 为什么这是最小且可演进的方案 - -修法 -- 先改哪一层 -- 调整哪些依赖或状态归属 - -验证 -- 如何证明边界和行为没有回归 -- 哪些风险尚未覆盖 -``` - -## 2. Bug 排查答法 -适用于:Crash、状态错乱、布局异常、并发问题、偶现问题。 - -输出结构: - -```text -结论 -- 最可能根因是什么 -- 出错落点在哪一层 - -为什么 -- 哪些证据支持这个判断 -- 为什么在这个时机触发 - -修法 -- 最小结构性修复怎么做 -- 为什么不是补丁式修法 - -验证 -- 如何复现和回归 -- 如何证明没有引入副作用 -``` - -## 3. 代码审查答法 -适用于:PR Review、方案 Review、重构 Review。 - -输出结构: - -```text -结论 -- 是否可合入 - -为什么 -- 按严重度列出正确性、架构、性能、验证问题 - -修法 -- 每个问题的最小修复建议 - -验证 -- 合入前缺哪些测试或验证 -``` - -执行要求: -- 严重问题先于风格问题。 -- 正确性先于可读性。 -- 风险先于偏好。 - -## 4. Swift 并发答法 -适用于:Actor 设计、任务取消、回调迁移、Sendable 审查。 - -输出结构: - -```text -结论 -- 并发边界应该怎么定 - -为什么 -- 当前风险点是什么 -- 哪个隔离或取消语义出了问题 - -修复方案 -- actor / `@MainActor` / Task 层级如何调整 -- 旧接口如何桥接 - -验证 -- 编译期并发检查 -- 真机行为验证 -- 取消链路验证 -``` - -## 5. 性能分析答法 -适用于:启动慢、滚动卡顿、内存上涨、页面刷新过重。 - -输出结构: - -```text -结论 -- 主要性能瓶颈是什么 -- 落在哪条关键路径 - -为什么 -- 哪些数据和热点支持这个判断 - -修法 -- 最小有效优化动作是什么 -- 哪些动作不应该现在做 - -验证 -- 优化前数据 -- 优化后数据 -- 是否有副作用 -``` - -## 6. 重构与迁移路线答法 -适用于:大型遗留模块拆分、UIKit 转 SwiftUI、回调迁移 async/await。 - -输出结构: - -```text -结论 -- 这次迁移或重构的目标和边界 - -为什么 -- 当前结构为什么必须调整 -- 最大风险点是什么 - -修法 -- 阶段如何切 -- 兼容层、调用迁移和删旧顺序如何安排 - -验证 -- 每阶段看什么信号 -- 回滚条件是什么 -``` - -## 7. 严格输出要求 -- 回答架构问题时,不只讲模式名称,必须讲边界、依赖方向和状态归属。 -- 回答 Bug 问题时,不只讲猜测,必须讲证据。 -- 回答性能问题时,不只讲优化点,必须讲指标。 -- 回答审查问题时,不只讲风格,必须讲风险。 -- 回答迁移问题时,不只讲终态,必须讲阶段。 -- 若没有必要,不额外扩展历史背景、教材说明或大段候选方案。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/execution_playbooks.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/execution_playbooks.md deleted file mode 100644 index 4fc4417..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/execution_playbooks.md +++ /dev/null @@ -1,114 +0,0 @@ -# 执行剧本 - -## 使用规则 -- 遇到复杂任务时,必须先选择对应剧本,再进入分析和实现。 -- 剧本定义的是执行顺序,不是背景知识说明。 -- 不得跳过“取证、边界、验证”三步。 -- 默认只展开当前选中的一个剧本,不并行套用多个剧本。 -- 输出时优先保留“当前在哪一步、下一步做什么、最终要验证什么”,不把整份剧本全文复述给用户。 - -> 排障类剧本同时遵守 [root_cause_enforcement.md](root_cause_enforcement.md) 根因纪律;并发 / 重构 / 迁移类剧本同时遵守 [migration_strategy.md](migration_strategy.md) 风险门禁。 - -## 目录 -- 接手遗留页面 -- 排查偶现 Crash -- 做一次性能优化 -- 做一次并发迁移 -- 做一次大型重构 - -## 接手遗留页面 -场景: -- 超大 ViewController / ViewModel -- 状态散落 -- UIKit / SwiftUI 混合老页面 - -步骤: -1. 定义页面边界:它负责什么,不负责什么。 -2. 识别状态来源:本地状态、远端状态、缓存状态、导航状态。 -3. 标出越界代码:网络、路由、缓存、埋点、权限、格式化。 -4. 建最小重构目标:先拆状态、再拆依赖、最后拆结构。 -5. 明确迁移阶段:不允许一次性大爆炸重构。 -6. 补测试和回归路径。 - -产物: -- 页面边界 -- 阶段顺序 -- 回归范围 - -## 排查偶现 Crash -场景: -- 难复现崩溃 -- 线上偶发异常 -- 随机状态错乱 - -步骤: -1. 定义现象:崩溃点、频率、设备、系统版本、触发条件。 -2. 建证据链:日志、调用栈、状态流、生命周期、线程/Actor。 -3. 区分崩溃点与根因。 -4. 沿输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染回溯。 -5. 做结构性修复,不做延迟、重试、判空补丁。 -6. 给出修复验证闭环和副作用评估。 - -产物: -- 根因 -- 修复前后证据 -- 复现与回归路径 - -## 做一次性能优化 -场景: -- 启动慢 -- 列表卡顿 -- 页面刷新重 -- 内存异常增长 - -步骤: -1. 明确指标:启动时长、FPS、主线程耗时、内存峰值、CPU。 -2. 锁定路径:冷启动、热启动、首屏、滚动、切换页面、后台切前台。 -3. 用工具取证:Time Profiler、Core Animation、Memory Graph、MetricKit。 -4. 找出最重热点,不同时处理多条主因。 -5. 明确优化动作:删除、下沉、异步化、缓存、瘦身。 -6. 对比优化前后数据,评估正确性和体验是否回归。 - -产物: -- 基线 -- 热点 -- 前后对比 - -## 做一次并发迁移 -场景: -- callback 迁 async/await -- GCD 迁结构化并发 -- 串行队列迁 actor - -步骤: -1. 列出当前并发模型:谁创建任务,谁写状态,谁切主线程。 -2. 列出共享可变状态和跨域传递数据。 -3. 先设计隔离域,再选 `@MainActor`、`actor`、`TaskGroup`、`async let`。 -4. 桥接旧接口时保证只 resume 一次。 -5. 建取消链路,阻止过期结果回写。 -6. 用编译检查、真机行为、取消验证确认迁移成功。 - -产物: -- 隔离模型 -- 迁移顺序 -- 取消与回写验证 - -## 做一次大型重构 -场景: -- 模块拆分 -- 导航重建 -- 状态模型重建 -- 网络层重构 - -步骤: -1. 定义重构目标和明确不做的范围。 -2. 写决策记录,比较候选方案。 -3. 划分阶段:建抽象、迁调用、删旧实现、补测试。 -4. 识别高风险模块和回滚点。 -5. 每阶段做行为一致性验证。 -6. 最后再清理历史兼容层。 - -产物: -- 决策记录 -- 阶段计划 -- 每阶段验证方法 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/layout_and_ui.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/layout_and_ui.md deleted file mode 100644 index a8d8941..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/layout_and_ui.md +++ /dev/null @@ -1,156 +0,0 @@ -# UI 布局与 HIG 规范 - -## 适用场景 -用于以下问题: -- Auto Layout 冲突、页面错位、列表高度异常 -- SwiftUI 视图抖动、跳动、刷新过多、导航状态错乱 -- Dark Mode、Dynamic Type、无障碍支持缺失 -- 高保真还原、复杂表单、复杂列表和混合布局 - -## UIKit 布局诊断顺序 -排查顺序固定为: -1. 视图层级是否合理 -2. 约束数量是否完整且无冲突 -3. `contentHugging` / `compressionResistance` 是否正确 -4. 是否错误依赖固定宽高 -5. 是否被复用、异步回填或隐藏逻辑影响 - -要求: -- 布局排查按以上顺序收敛,不并行罗列多个大候选方向。 -- 输出时优先指出当前最可能断链点,再补充次要可能性。 - -### UIKit 约束规则 -- 非必要场景不得使用 `999` 这类“接近必选”的优先级掩盖设计问题;只有在明确说明约束意图且常规约束方案不成立时才允许使用。 -- 约束先表达相对关系和内容驱动链路,不先依赖写死宽高、魔法间距或补丁式尺寸。 -- 出现约束冲突时,先修正视图层级和约束设计,不先通过调优优先级规避问题。 -- 通过完整约束关系表达布局,不靠 `layoutIfNeeded()` 硬催。 -- 复杂 Cell 要明确内容边界、间距来源和自适应高度链路。 -- 自适应高度必须能解释清楚由谁撑开、约束如何闭合、何处可能因隐藏或复用断链。 -- 不在 `layoutSubviews`、`updateConstraints` 或同类高频生命周期里反复创建、激活或重建约束。 -- 使用 Auto Layout 时,必须明确 `translatesAutoresizingMaskIntoConstraints` 的开启或关闭语义,避免系统约束和手写约束混杂失控。 -- `UIStackView` 适合线性布局,不适合承载复杂、条件分支很多的页面骨架。 - -### 自适应内容 -- 依赖 `intrinsicContentSize` 和约束链路实现自适应。 -- 文本、多语言、超长文案、极端字号必须纳入验证范围。 -- 列表高度计算要考虑异步图片、富文本、展开收起和复用回写。 - -## SwiftUI 视图设计规则 -### 状态管理 -- 将状态粒度压低,避免根 View 持有过大的可变状态。 -- 不把网络请求、埋点、导航副作用直接写在 `body` 的临时闭包里。 -- 必须保证 `id` 稳定,避免列表闪烁、滚动位置丢失、视图状态错位。 - -### 布局稳定性 -- 必须理解 `frame`、`fixedSize`、`layoutPriority`、`alignment` 的语义,禁止层层叠 modifier 试错。 -- 避免不必要的 `GeometryReader` 扩散。 -- 针对复杂滚动页,评估 `LazyVStack`、分段加载和子视图拆分。 - -## 列表与复用 -- UIKit 列表关注复用标识、异步任务取消、图片回填错位、状态残留。 -- SwiftUI 列表关注身份稳定、最小刷新范围和数据源 diff 质量。 -- 任何列表问题都要同时检查“数据源、复用链路、异步回填、布局约束”四条线。 - -## 自动布局补充检查 -- 多行文本、自适应高度、长文案、多语言和极端字号视为默认验证项,不是额外加测项。 -- 隐藏、折叠、展开、占位切换和异步内容回填后,必须重新检查约束链路是否仍然闭合。 -- 对嵌套滚动、复杂表单、动态列表页,先判断是否是层级设计问题,再判断是否是单条约束问题。 -- SwiftUI 出现跳动、闪烁、错位时,同时检查 `id` 稳定性、状态粒度和刷新边界,不把所有现象都归因于布局。 - -## Apple HIG 与可访问性 -### 基本要求 -- 使用语义色、动态字体和系统交互反馈。 -- 交互区域、层级层次、返回路径和空状态要符合 iOS 用户习惯。 -- 不为了“像设计稿”而破坏平台交互一致性。 - -### 无障碍要求 -- 关键控件提供准确的 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。 -- 焦点顺序、朗读内容和可点击区域必须可用。 -- 图片和图标要区分装饰性资源与有语义资源。 - -## 常见反模式 -- 通过写死宽高、额外加空白 View、疯狂调优先级解决布局问题。 -- 在 Cell/Item 复用场景里忘记重置状态和取消异步任务。 -- 在 `layoutSubviews` 或约束更新回调中不断重建约束,导致抖动、冲突或性能退化。 -- 把 Auto Layout 问题简化成“多调几个优先级总能过”。 -- SwiftUI 中把多个业务状态塞进一个大对象,导致整页刷新。 -- 为赶进度忽略 Dark Mode、Dynamic Type、VoiceOver。 - -## UITableView 发送消息置顶(Pin-to-top on send) - -### 适用场景 -聊天列表中用户发送消息后,需要将该用户消息显示在屏幕顶部,同时 bot 响应在其下方向下生长。 - -### 核心机制:contentInset.bottom 补偿(参考 MainContentViewCollection.pinMessageToTop) -**禁止**用 `scrollToRow(at:, at: .top)` 强制置顶——它无法与流式响应的 `scrollToBottom` 兼容。 -**正确方案**:补偿 `contentInset.bottom`,使 `scrollToBottom` 后用户消息恰好落在视口顶部。 - -```swift -// 1. 发送时仅插入最后一行(不走 reloadData,避免全量刷新位移跳动) -UIView.performWithoutAnimation { - self.tableView.insertRows(at: [lastIndexPath], with: .none) -} -// 2. 强制完成布局,确保 rectForRow 有效 -self.tableView.layoutIfNeeded() -// 3. 取用户消息的 rect,计算从其顶部到内容末尾的高度 -let userRect = self.tableView.rectForRow(at: userIndexPath) -let heightFromUserToEnd = self.tableView.contentSize.height - userRect.minY -let viewportHeight = self.tableView.bounds.height - - self.tableView.adjustedContentInset.top - - self.tableView.adjustedContentInset.bottom -// 4. 补偿 bottom inset,让 scrollToBottom 后用户消息恰好贴顶 -let needed = max(0, viewportHeight - heightFromUserToEnd) -if needed > 0.5 { - self.tableView.contentInset.bottom += needed -} -// 5. 执行 scrollToBottom(isPinnedToBottom = true 保证流式响应继续自动跟随) -self.scrollToLatest(animated: false) -``` - -### 状态机设计 -- `isPinnedToBottom: Bool`:是否处于"底部跟随"模式(发送后置为 true,让流式响应继续自动下滚)。 -- `pendingForceScroll: Bool`:发送时设为 true,下次 reloadData 触发置顶插入逻辑。 -- `pinExtraBottomInset: CGFloat`:记录本次补偿量,响应结束或手动滚底时用 `clearPinExtraInset()` 还原。 -- `pinRetryToken: UUID`:置顶重试链的失效令牌,响应结束时更新,旧重试任务自动失效。 - -**禁止**用多个 Bool 拼状态(如同时维护 `isPinnedToTop` + `isPinnedToBottom`),应收敛到 `pinExtraBottomInset > 0` 作为"置顶激活"的唯一信号。 - -### 重试机制(等待 cell 布局就绪) -`rectForRow` 返回零高说明 cell 尚未完成布局,需重试: - -```swift -private func pinLastUserMessageToTop(retryToken: UUID, remainingAttempts: Int = 3) { - guard retryToken == self.pinRetryToken else { return } - // ...取 userRect... - guard userRect.height > 0.5 else { - guard remainingAttempts > 1 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { [weak self] in - self?.pinLastUserMessageToTop(retryToken: retryToken, remainingAttempts: remainingAttempts - 1) - } - return - } - // ...执行补偿和滚动... -} -``` - -### 生命周期清理 -| 时机 | 操作 | -|---|---| -| 响应结束(`endLoading`)| `clearPinExtraInset()` + `invalidatePinRetryToken()` | -| 用户手动点"↓"滚到底 | `clearPinExtraInset()` + `invalidatePinRetryToken()` + `scrollToLatest()` | -| 用户手动滑到底部(`scrollViewDidScroll`)| 无需额外操作,`isPinnedToBottom = true` 自然接管流式跟随 | - -### 常见陷阱 -- **不能用 `scrollToRow(at: .top)`**:发送后流式响应的每次 `reloadData` 都会 `scrollToBottom`,覆盖置顶。 -- **`cellForRow(at:)` 检查 cell 高度不可靠**:新插入 cell 未进入可视区时永远返回 nil,导致重试全部失败。正确做法是用 `rectForRow`(即使 cell 不可见也能返回布局数据)。 -- **`reloadData` 会触发 `contentOffset` 重置**:用户消息插入时必须用 `insertRows`,否则已有内容的视觉位置会跳动。 -- **补偿 inset 必须在响应结束后还原**:不还原会导致列表底部出现永久空白。 - -## 审查清单 -- [ ] 布局是否由明确约束或明确的 SwiftUI 布局语义驱动? -- [ ] 是否兼容长文本、多语言、极端字号和深色模式? -- [ ] 列表或表单是否考虑了复用、回填、焦点和滚动稳定性? -- [ ] 是否存在身份不稳定、过度刷新或错误的状态归属? -- [ ] 是否补齐了无障碍和平台一致性要求? -- [ ] 聊天列表置顶:是否用 contentInset.bottom 补偿而非 scrollToRow(.top)? -- [ ] 聊天列表置顶:响应结束后是否清除了补偿 inset 和重试 token? diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/mcp_control.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/mcp_control.md deleted file mode 100644 index 98d0ed0..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/mcp_control.md +++ /dev/null @@ -1,50 +0,0 @@ -# MCP 与工具调用控制 - -## 目录 -- 使用规则 -- 自动问题归一化 -- 调用预算 -- 重试与限流 -- 上下文压缩 -- 防循环退出条件 - -## 使用规则 -- 涉及 MCP、搜索、日志取证、多轮排查、复杂工具调用时,必须使用本文件。 -- 目标是减少无效工具调用、限制上下文膨胀、避免重复尝试同一路径。 -- 本文件只约束执行预算和停损条件,不重复定义根因分析和输出模板。 - -## 调用预算 -- 开始调用工具前,先判断当前任务属于轻任务、常规修复还是复杂排障,再选择对应预算。 -- 工具调用预算分层控制: - - 轻任务:最多 6 次 - - 常规修复:最多 10 次 - - 复杂排障、迁移或跨模块问题:最多 15 次 -- 只有在已经拿到新证据时才继续扩展预算,不因“还没想明白”而无限追加调用。 -- 同类工具连续调用最多 2 次,例如连续搜索、连续打开多个无新增信息的页面、连续读取同类型日志。 -- 同时只允许 1 个主调查方向,最多保留 1 个备选方向。 -- 读取文件时先读最相关、最短路径的文件,不先全量扫目录或批量读大文件。 - -## 重试与限流 -- 同一个工具、同一参数失败两次后,不得第三次原样重试。 -- 若继续尝试,必须先改变一个条件:参数、范围、入口、证据来源或假设方向。 -- 连续两次搜索没有新增证据后,停止搜索,先总结已知事实和缺口。 -- 连续两次读取不同文件仍无法支持当前假设后,回退并重审根因假设。 - -## 上下文压缩 -- 连续 2 到 3 轮后,先压缩为四段再继续: - - 现象 - - 已知事实 - - 已排除项 - - 下一步 -- 压缩后不重复带入已失效假设、已关闭分支和无关历史背景。 - -## 防循环退出条件 -满足任一条件,就必须切换方向或暂停继续同一路径: -- 同一路径验证失败 2 次。 -- 同一个搜索方向连续 2 次没有新增证据。 -- 同一文件围绕同一问题来回修改 2 次仍无验证进展。 -- 同一根因假设无法解释新增现象或新增证据。 - -## 输出要求 -- 工具调用后的结论优先输出:拿到了什么新证据、排除了什么、下一步做什么。 -- 若因预算或防循环规则停止当前路径,必须明确说明停止原因。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/migration_strategy.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/migration_strategy.md deleted file mode 100644 index fac847e..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/migration_strategy.md +++ /dev/null @@ -1,139 +0,0 @@ -# 迁移策略与风险控制 - -## 目录 -- 适用场景 -- 使用规则 -- 重构原则 -- 巨型文件拆分策略 -- 迁移策略 -- 风险识别 -- 阶段化迁移 -- 兼容层策略 -- 灰度与回滚 -- 验证策略 -- 发布前检查 -- 审查输出标准 -- 常见反模式 - -## 适用场景 -用于以下任务: -- 遗留项目治理、巨型文件拆分、架构清理 -- 回调地狱迁移到 `async/await` -- GCD 迁结构化并发、串行队列迁 `actor` -- UIKit 与 SwiftUI 混合改造 -- 网络层、缓存层、鉴权层重构 -- Pull Request 审查、技术方案审查、重构路线设计 - -## 使用规则 -- 涉及架构迁移、模块拆分、并发模型改造、网络层重构、UIKit 向 SwiftUI 迁移时,必须使用本文件。 -- 迁移不是单次代码替换,而是持续风险控制过程。 -- 不得在没有回滚条件、兼容层策略和验证路径时推进高风险迁移。 -- 重构与迁移必须同时处理"如何改"和"如何控风险",不得只答一面。 -- 相关剧本见 [execution_playbooks.md](execution_playbooks.md);发布与 CI 门禁见 [build_release_and_ci.md](build_release_and_ci.md)。 - -## 重构原则 -- 先稳住行为,再调整结构;禁止一边重构一边无边界改需求。 -- 采用可验证的小步重构,禁止一次性"大爆破"。 -- 重构目标必须明确:降耦合、提测试性、消灭重复、收敛状态、明确边界。 - -## 巨型文件拆分策略 -### ViewController / ViewModel 过大 -- 先识别哪些是渲染、哪些是业务编排、哪些是数据访问、哪些是路由。 -- 提取列表数据源、表单校验、网络编排、路由跳转、埋点逻辑。 -- 通过协议切面和依赖注入拆分,而不是简单把代码挪到 `Extensions` 里。 - -### Service / Manager 失控 -- 若一个对象同时负责网络、缓存、埋点、权限、状态同步,必须拆分职责。 -- 先抽出稳定抽象,再迁移调用方,最后删除旧实现。 - -## 迁移策略 -### 回调到 async/await -- 先从边缘依赖开始包一层异步接口,再逐步向上收敛调用链。 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须保证只 resume 一次。 -- 迁移期间禁止混用多套取消语义导致行为不一致。 - -### GCD 到结构化并发 -- 把"队列"问题翻译为"隔离域"和"任务层级"问题。 -- 串行队列保护共享状态时,评估是否应改为 `actor`。 -- `DispatchSemaphore`、`group.wait()` 一类阻塞式方案视为高风险。 - -### UIKit 与 SwiftUI 混合迁移 -- 先决定谁是宿主,谁是增量引入方。 -- 避免同时迁移 UI、状态管理、导航和网络层,拆成多个阶段。 -- 对可复用组件抽成独立模块,禁止散落双端实现。 - -## 风险识别 -- 开始前必须识别影响范围:页面、模块、共享组件、埋点、缓存、测试、发布路径。 -- 必须识别最容易出问题的链路:启动、登录、列表、支付、提交、深链路导航。 -- 必须明确迁移后的新风险,而不是只描述旧问题。 - -## 阶段化迁移 -所有高风险迁移必须拆成阶段: -1. 建抽象 -2. 接兼容层 -3. 迁调用方 -4. 删除旧实现 -5. 收口验证 - -要求: -- 每个阶段都必须有独立可验证的交付结果。 -- 不得把"建抽象、迁调用、删旧实现"压在一次提交中完成。 - -## 兼容层策略 -- 兼容层必须有明确生命周期:为什么存在、服务谁、何时删除。 -- 兼容层必须限制扩散范围,不得成为新的长期依赖。 -- 引入双写、双读、双路由、双渲染时,必须定义一致性检查方式。 - -## 灰度与回滚 -- 高风险迁移必须明确灰度范围。 -- 必须明确回滚触发条件:Crash、关键指标异常、业务失败率上升、性能显著退化。 -- 回滚路径必须可执行,不得只写"有问题就回滚"。 -- 功能开关、路由开关、配置开关必须职责清晰。 - -## 验证策略 -- 每个阶段都必须定义:验证目标、验证范围、验证方式、未覆盖风险。 -- 必须覆盖新旧链路一致性验证。 -- 必须覆盖异常路径和降级路径。 -- 若迁移涉及并发和状态模型,必须专项验证取消、回写、隔离和回归。 - -## 发布前检查 -- 是否已识别影响面和高风险链路 -- 是否已定义兼容层和删除条件 -- 是否已具备灰度和回滚手段 -- 是否已补齐关键测试和观测指标 -- 是否已明确失败信号和负责人 - -## 审查输出标准 -代码审查必须先指出: -- 正确性问题:Crash、竞态、状态错乱、生命周期错误 -- 架构问题:越界、耦合、不可测试、不可替换 -- 性能问题:主线程阻塞、过度刷新、列表复用失效 -- 质量问题:命名、抽象、重复逻辑、缺失验证 - -### 审查结论格式 -- 问题是什么 -- 为什么是问题 -- 影响范围 -- 推荐修法 -- 是否需要补测试或验证 - -## 常见反模式 -- 把重构等同于"拆文件"而不是"重建边界"。 -- 没有回归验证就大规模迁移并发模型。 -- 用新框架包裹旧问题,结果只是把复杂度换了位置。 -- 代码审查只提风格意见,不提正确性、风险和验证。 -- 一次性大迁移,不分阶段。 -- 没有兼容层就直接切主链路。 -- 引入兼容层后无限期不删除。 -- 没有灰度,只能全量上线。 -- 没有回滚路径就推进重构。 -- 发布前没有定义指标和失败信号。 - -## 验证清单 -- [ ] 是否定义了重构范围、目标和不变行为? -- [ ] 是否分阶段推进,并保留了回归验证手段? -- [ ] 是否先建立抽象,再迁移实现和调用方? -- [ ] 是否识别了影响面、高风险链路和兼容层生命周期? -- [ ] 是否具备灰度和可执行的回滚路径? -- [ ] 并发迁移后是否验证了取消、线程隔离和状态一致性? -- [ ] 审查意见是否覆盖正确性、架构、性能和测试? diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/networking_patterns.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/networking_patterns.md deleted file mode 100644 index 957bbd8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/networking_patterns.md +++ /dev/null @@ -1,124 +0,0 @@ -# 网络模式 - -## 目录 -- 使用规则 -- 请求链路 -- 分页模式 -- 重试模式 -- 缓存模式 -- 鉴权刷新模式 -- 上传下载模式 -- 幂等与去重 -- 错误分层 -- 常见反模式 - -## 使用规则 -- 涉及分页、缓存、重试、鉴权、上传下载、请求去重时,必须使用本文件定义的模式。 -- 不得把网络问题简化成“发请求并解析 JSON”。 -- 任何网络模式都必须说明边界、失败策略和验证方式。 - -## 请求链路 -固定链路: - -```text -Endpoint -> RequestBuilder -> APIClient -> DTO -> Repository -> Entity -> ViewModel -``` - -要求: -- Endpoint 定义路径、方法、查询参数、Header、Body。 -- RequestBuilder 负责构造 `URLRequest`。 -- APIClient 负责发送、解码、错误分层。 -- Repository 负责聚合网络、缓存、持久化与映射。 - -## 分页模式 -### Page-based -适用于: -- 明确页码和页大小的接口 - -要求: -- 状态中显式保存当前页、是否还有下一页、是否正在分页。 -- 首刷、下拉刷新、加载更多三条路径分别建模。 - -### Cursor-based -适用于: -- 流式列表、时间线、游标接口 - -要求: -- 显式保存 `nextCursor`。 -- 不得把空游标和第一页混为一谈。 - -### 分页统一要求 -- 不得重复发下一页请求。 -- 不得让过期分页结果覆盖新刷新结果。 -- 必须验证空页、尾页、重复触发分页三种路径。 - -## 重试模式 -- 只允许对幂等请求做自动重试。 -- 必须定义最大重试次数、退避策略和终止条件。 -- 网络不稳定与业务失败必须区分,业务失败不得静默重试。 - -适合重试: -- 获取配置 -- 拉取列表 -- 查询详情 - -不适合重试: -- 下单 -- 支付 -- 表单提交 -- 不具备幂等保证的写操作 - -## 缓存模式 -### 展示缓存 -- 用于首屏提速和弱网兜底。 - -### 业务缓存 -- 用于降低重复请求和控制读取成本。 - -### 离线缓存 -- 用于断网可读或延迟同步场景。 - -统一要求: -- 必须定义缓存键。 -- 必须定义失效条件。 -- 必须定义写入时机和清理策略。 -- 不得让 ViewModel 直接感知缓存实现细节。 - -## 鉴权刷新模式 -- Token 刷新必须串行化。 -- 并发请求命中过期 Token 时,不得同时触发多次刷新。 -- 刷新失败必须明确退出策略:重登、降级、只读、提示。 -- 刷新逻辑不得散落在各个业务 Service。 - -## 上传下载模式 -- 上传下载必须有状态建模:等待中、进行中、成功、失败、取消。 -- 大文件任务必须支持取消、重试和进度上报。 -- 后台上传下载必须明确系统约束和恢复策略。 -- 文件路径、临时文件、磁盘占用必须纳入生命周期治理。 - -## 幂等与去重 -- 所有写操作都要先判断幂等性要求。 -- 相同请求在短时间内重复触发时,必须定义去重策略或合并策略。 -- 提交类操作必须防止用户重复点击和网络抖动导致重复提交。 - -## 错误分层 -固定分层: -- 传输错误 -- 状态码错误 -- 解码错误 -- 鉴权错误 -- 业务错误 -- 展示错误 - -要求: -- 每层错误都必须有明确归属。 -- 不得直接把底层错误文本暴露给用户。 -- 面向 UI 的错误必须说明用户可执行动作。 - -## 常见反模式 -- 一个 `NetworkManager` 承担所有职责 -- 在 ViewModel 中直接拼请求和解析 DTO -- 无条件自动重试 -- 缓存没有失效策略 -- Token 刷新并发失控 -- 上传下载没有取消和恢复设计 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/observability_logging.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/observability_logging.md deleted file mode 100644 index 5213a24..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/observability_logging.md +++ /dev/null @@ -1,87 +0,0 @@ -# 可观测性与日志 - -## 目录 -- 使用规则 -- 观测目标 -- 日志分层 -- 必记字段 -- 性能观测 -- 排障取证 -- 埋点纪律 -- 隐私与安全 -- 常见反模式 - -## 使用规则 -- 涉及 Bug 排查、性能优化、并发问题、网络异常、状态错乱时,必须先补齐可观测性。 -- 没有日志、没有指标、没有证据链的问题,不得宣称已定位。 -- 日志和埋点必须服务于排障、验证和回归,不得变成噪音堆积。 - -## 观测目标 -可观测性必须回答: -- 发生了什么 -- 在什么时机发生 -- 由谁触发 -- 在哪个线程 / Actor / Task 发生 -- 影响了什么状态和页面 -- 是否可复现 - -## 日志分层 -固定分为四层: -- 输入日志:用户动作、外部事件、接口响应 -- 状态日志:状态切换、关键属性变化、任务创建与取消 -- 生命周期日志:页面进入离开、对象 init/deinit、任务开始结束 -- 错误日志:失败分支、异常路径、重试、降级、断言信息 - -要求: -- 日志必须可追踪同一条业务链路。 -- 相同链路日志必须带统一标识。 -- 关键失败路径不得只打一条“失败了”的无效日志。 - -## 必记字段 -关键日志至少包含: -- 事件名 -- 模块名 / 页面名 -- 请求标识 / 任务标识 -- 当前线程或 Actor 上下文 -- 关键输入参数摘要 -- 关键状态变化 -- 结果或错误分类 -- 时间戳 - -## 性能观测 -- 启动、首屏、页面切换、列表滚动、图片加载、网络请求必须可量化。 -- 性能数据必须能区分冷启动、热启动、弱网、低端机。 -- 关键路径需要配合 `OSLog`、Points of Interest 或 MetricKit 观测。 - -必须观测的常见指标: -- 启动时长 -- 首屏可交互时长 -- 列表滚动帧率 -- 主线程热点 -- 内存峰值 -- 请求耗时和失败率 - -## 排障取证 -- Bug 排查时,日志必须覆盖输入、状态、生命周期、线程/Actor、错误分支。 -- 并发问题必须记录任务创建、取消、回写和丢弃时机。 -- 列表问题必须记录刷新、分页、复用、回填、身份变化。 -- 崩溃问题必须关联调用栈、关键状态和最后一次有效操作链路。 - -## 埋点纪律 -- 埋点用于行为分析,不替代排障日志。 -- 埋点名称、参数和时机必须稳定,不得随意改写。 -- 同一业务动作只埋一次主事件,不重复轰炸。 -- 埋点字段必须有明确业务语义,不得堆积无解释参数。 - -## 隐私与安全 -- 禁止记录 Token、密码、身份证号、完整手机号、完整支付信息。 -- 需要排障时只记录脱敏摘要。 -- 用户隐私数据的观测必须符合产品和合规要求。 - -## 常见反模式 -- 只在 `catch` 里打印一句 error -- 日志没有链路标识,无法串联 -- 并发问题没有记录任务创建、取消、回写 -- 性能优化没有基线数据 -- 埋点和日志职责混乱 -- 为了排障打印敏感数据 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/performance_optimization.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/performance_optimization.md deleted file mode 100644 index a953057..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/performance_optimization.md +++ /dev/null @@ -1,73 +0,0 @@ -# 性能优化 - -## 适用场景 -用于分析和优化: -- 启动慢、首屏慢、页面切换慢 -- 列表卡顿、掉帧、滚动不稳 -- SwiftUI 过度刷新、UIKit 渲染成本高 -- 内存上涨、对象泄漏、频繁峰值 -- 高耗电、后台任务失控、图片和网络开销过大 - -## 总原则 -- 先量化,再优化;没有指标,不做拍脑袋优化。 -- 先解决主线程阻塞、重复计算、无效刷新和资源浪费。 -- 优化必须有前后对比数据,并确认没有引入行为回归。 - -## 性能排查顺序 -1. 明确问题指标:启动时长、帧率、主线程耗时、内存峰值、CPU、能耗 -2. 确定触发路径:冷启动、热启动、特定页面、滚动、网络回包、后台切前台 -3. 用工具取证:Instruments、Memory Graph、OSLog、MetricKit -4. 定位主因后再决定是架构调整、缓存、异步化还是渲染瘦身 - -## SwiftUI 优化要点 -### 刷新范围 -- 先检查是谁触发了 `body` 重算,而不是一味拆 View。 -- 降低状态辐射范围,避免根节点持有过大可变对象。 -- 对可比较的输入考虑 `Equatable` 或更稳定的值语义模型。 - -### 列表与大数据量 -- 大数据量使用惰性容器。 -- 保证 `id` 稳定,避免 diff 失效导致重建。 -- 图片加载、分页、预取、占位策略必须一起评估。 - -## UIKit 优化要点 -### 滚动与渲染 -- 减少视图层级和约束复杂度。 -- 检查离屏渲染、透明混合、阴影、圆角和遮罩组合的成本。 -- Cell 内避免重复创建格式化器、富文本解析器和重量级对象。 - -### 任务调度 -- 主线程只做必须在主线程完成的事。 -- 数据整形、预计算、图片解码、日志整理移出主线程。 -- 注意异步化不是万能,重点是避免主线程等待和回切抖动。 - -## 启动优化 -- 冷启动先压缩启动路径上的同步 IO、同步网络、重量级单例初始化。 -- 首屏只加载首屏必须数据,延迟非关键能力。 -- 避免在 `AppDelegate` / `SceneDelegate` / 根页面初始化阶段做过多全局注册。 - -## 内存治理 -- 关注缓存是否可控、图片是否过大、列表是否持有过多中间对象。 -- 排查闭包循环引用、Task 生命周期、通知未释放、观察者未移除。 -- 优化时同时关注峰值和稳态,而不是只看瞬时分配。 - -## 常用工具 -- `Time Profiler`:定位 CPU 和主线程热点 -- `Core Animation`:观察帧率、混合和渲染压力 -- `Allocations` / `Leaks` / `Memory Graph`:分析内存增长和引用关系 -- `Points of Interest` / `OSLog`:补齐关键链路耗时标记 -- `MetricKit`:关注线上崩溃、卡顿和能耗趋势 - -## 常见反模式 -- 没有指标就盲目“优化”代码风格。 -- 为了避免一次计算,把状态和缓存散得到处都是。 -- SwiftUI 页面一个状态变化导致整页重绘。 -- UIKit 列表在主线程做解码、排版、图片处理和高度计算。 -- 只优化实验环境,不验证真实设备和弱网场景。 - -## 验证清单 -- [ ] 是否给出了可复现路径和性能指标? -- [ ] 是否有优化前后的量化对比? -- [ ] 是否确认主线程热点、刷新范围或内存热点已经下降? -- [ ] 是否验证了低端机、长列表、弱网、后台切前台等场景? -- [ ] 是否避免为了性能引入可维护性和正确性回归? diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/review_checklists.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/review_checklists.md deleted file mode 100644 index 13bdb0f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/review_checklists.md +++ /dev/null @@ -1,90 +0,0 @@ -# iOS Review 检查表 - -## 使用规则 -- 做代码审查、方案审查、重构审查时,必须按本清单逐项过检。 -- 审查结论必须覆盖正确性、架构、并发、性能、UI、测试六个维度。 -- 发现严重问题时,必须明确标记“不可合入”。 - -## 1. 正确性检查 -- [ ] 是否存在强制解包、越界、非法状态转换或空数据假设? -- [ ] 是否存在错误的生命周期依赖? -- [ ] 是否存在异步回写过期数据的问题? -- [ ] 是否存在列表复用导致的状态残留? -- [ ] 是否存在错误处理缺失或错误吞没? -- [ ] 新增字段、参数或状态是否已经沿完整调用链补齐真实数据来源,而不是只在局部声明变量或临时透传让当前代码通过? -- [ ] 当前修复是否引入新的 Bug、行为回归或隐性风险? - -## 2. 架构检查 -- [ ] View / ViewController 是否越界承载业务逻辑? -- [ ] ViewModel / UseCase / Repository / Service 职责是否清晰? -- [ ] 依赖是否面向协议而不是具体实现? -- [ ] 模块边界是否清楚?是否存在跨模块偷渡? -- [ ] 路由是否放在 Coordinator / Router,而不是页面内部硬编码? -- [ ] 若新增值依赖上游透传,是否已经回溯到真实拥有者、构造点和映射层,而不是把中间层变成无语义的参数搬运站? - -## 3. 并发检查 -- [ ] UI 更新是否全部受 `@MainActor` 约束? -- [ ] 是否存在共享可变状态未隔离的问题? -- [ ] 是否存在无归属 `Task {}`? -- [ ] 是否有任务取消遗漏、取消后回写、竞态覆盖? -- [ ] `Sendable`、`actor`、桥接旧接口的使用是否真实安全? - -## 4. 性能检查 -- [ ] 是否把重计算、解码、排序、IO 放到了主线程? -- [ ] 是否存在 SwiftUI 过度刷新或 UIKit 层级过深问题? -- [ ] 列表滚动路径是否存在明显热点? -- [ ] 是否引入了不必要缓存、重复计算或重复请求? -- [ ] 是否给出了性能验证数据? - -## 5. UI / UX / 无障碍检查 -- [ ] 是否兼容长文本、多语言、极端字号和 Dark Mode? -- [ ] 布局是否依赖硬编码尺寸或魔法间距? -- [ ] 是否保证列表身份稳定和交互状态一致? -- [ ] 是否具备基础无障碍语义? -- [ ] 是否破坏平台交互一致性? - -## 6. 测试与验证检查 -- [ ] 是否补了关键业务逻辑单元测试? -- [ ] 是否定义了集成验证路径? -- [ ] Bug 修复是否有复现路径和修复证明? -- [ ] Bug 修复是否验证了未引入新的 Bug、回归或副作用? -- [ ] 性能优化是否有前后对比? -- [ ] 重构迁移是否有阶段性回归验证? - -## 7. 审查结论级别 -### 不可合入 -满足任一条件即判定: -- 会导致 Crash、数据错乱、严重竞态、严重泄漏 -- 明显架构越界且后续难以收口 -- 修复没有根因证据,属于补丁式方案 -- 为修复当前问题引入了新的 Bug、回归或隐性风险 - -### 可修改后合入 -适用于: -- 结构可接受,但存在局部实现缺陷 -- 测试、验证、边界处理不完整 - -### 可合入 -适用于: -- 正确性、架构、并发、性能、UI、测试均过检 -- 剩余问题只属于低风险优化项 - -> 常见反模式对照见 [anti_patterns.md](anti_patterns.md);跨模块协作 / PR 拆分 / ownership 审查规则见 [team_collaboration.md](team_collaboration.md)。 - -## 8. 标准输出骨架 -```text -审查结论 -- 不可合入 / 可修改后合入 / 可合入 - -严重问题 -1. ... - -一般问题 -1. ... - -验证缺口 -- ... - -最终要求 -- 合入前必须完成什么 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/root_cause_enforcement.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/root_cause_enforcement.md deleted file mode 100644 index 1ae9d68..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/root_cause_enforcement.md +++ /dev/null @@ -1,111 +0,0 @@ -# 根因修复铁律 - -## 目录 -- 核心原则 -- 排障标准流程 -- 明确禁止的“伪修复” -- 证据要求 -- 修复后必须评估的副作用 -- 验证要求 - -所有排障、修复、重构建议都必须服从本文件。它只定义排障纪律、证据标准和伪修复禁令,不重复定义通用输出模板或工具预算。 - -## 核心原则 -- 没有证据,不下结论。 -- 没有边界,不开始修复。 -- 没有根因,不提交补丁。 -- 没有验证,不宣布完成。 -- 修复当前问题时,禁止引入新的问题、回归或隐性风险。 -- 默认先追 1 个最高概率根因,不同时展开多个大分支消耗上下文和 token。 - -## 排障标准流程 -### 1. 定义问题边界 -开始前必须明确: -- 现象是什么 -- 触发条件是什么 -- 影响范围有多大 -- 是否稳定复现 -- 设备、系统版本、网络环境和并发环境 - -### 2. 建立证据链 -必须至少从下列维度取证: -- 调用链路 -- 状态流转 -- 生命周期 -- 线程 / Actor / Task 上下文 -- 内存引用关系 -- 日志、断点、调用栈、Instruments - -取证策略: -- 优先补齐最能区分主假设和次假设的证据,不把所有可能性一次性铺开。 -- 若当前证据不足以区分多个方向,先提出 1 个最关键确认问题,而不是并行展开长篇猜测。 - -### 3. 沿全链路回溯 -固定沿以下链路回溯: - -```text -输入源 -> 数据转换 -> 状态管理 -> 并发边界 -> 生命周期 -> UI 渲染 -> 用户可见现象 -``` - -禁止只在报错点或 View 层就地修补。 - -### 4. 实施结构性修复 -修复落在: -- 架构边界 -- 状态模型 -- 数据流 -- 并发隔离 -- 生命周期管理 - -### 5. 验证并沉淀 -修复后必须补齐: -- 可复现的验证路径 -- 修复前后对比证据 -- 必要测试 - -## 明确禁止的“伪修复” -以下方式一律判定为掩盖问题: -- 新增兜底 `if` -- `DispatchQueue.main.async` / `asyncAfter` 拖延时序 -- 反复 `reloadData`、`setNeedsLayout`、`layoutIfNeeded` -- 增加临时布尔标记位压住现象 -- 多写一层容错分支但不解释结构原因 -- 靠重试、延迟、判空碰运气 - -若确实需要降级策略,必须先说明真实根因和为什么当前阶段只能降级。 - -## 证据要求 -### 日志最少覆盖 -| 类别 | 说明 | -|------|------| -| 输入 | 入参、外部事件、服务端响应 | -| 状态 | 状态切换、关键属性变更 | -| 上下文 | 线程、Actor、Task、队列 | -| 生命周期 | `init`、`deinit`、页面生命周期 | -| UI 触发点 | 刷新来源、绑定更新、复用时机 | -| 异常路径 | `guard`、`catch`、失败分支 | - -### 结论要求 -- 现象不等于根因。 -- 崩溃点不等于根因,最后一个报错栈帧经常只是受害者。 -- 根因必须能解释“为什么会发生”和“为什么在这个时机发生”。 - -> 并发相关证据链(任务创建 / 取消 / 过期回写)建模见 [swift_concurrency.md](swift_concurrency.md);日志分层、必记字段、链路标识见 [observability_logging.md](observability_logging.md)。 - -## 修复后必须评估的副作用 -- 是否改变状态流和业务语义 -- 是否引入新的竞态或线程切换问题 -- 是否影响性能、滚动、启动或耗电 -- 是否影响对象释放、任务取消和复用链路 -- 是否波及其他页面或共享组件 -- 是否为了修复当前问题而引入新的 Bug 或回归 - -## 验证要求 -至少组合使用以下一种或多种方式: -- 单元测试 -- 集成测试 -- 真机复现 -- 日志断点 -- Memory Graph -- Instruments -- 并发检查工具 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/self_evolution.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/self_evolution.md deleted file mode 100644 index b94f8c8..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/self_evolution.md +++ /dev/null @@ -1,122 +0,0 @@ -# Skill 自进化治理 - -## 目录 -- 使用规则 -- 触发信号 -- 自进化闭环 -- 候选版约束 -- 自动验证门禁 -- 晋升与回滚 -- 明确禁止的模式 -- 提案模板 - -## 使用规则 -- 只有在真实任务中发现当前 skill 存在规则缺失、规则冲突、规则重复、规则失效或输出失真时,才使用本文件。 -- 本文件定义的是 skill 的受控自进化流程,不是业务问题的答法模板。 -- 默认生成候选改动并验证,不直接把未验证的规则改动当作新的生效版本。 -- 版本状态保存在 `evolution/active_version.json`;提案、验证记录、授权记录、历史快照分别存放在 `evolution/proposals/`、`evolution/validations/`、`evolution/approvals/` 和 `evolution/history/`。 - -## 触发信号 -以下信号满足任一条,就可以进入自进化流程: -- 同类问题连续出现,而现有规则没有覆盖。 -- 现有规则可以覆盖,但表达不清,导致执行结果持续偏移。 -- 多份文档对同一件事重复下定义,导致上下文膨胀或优先级冲突。 -- 某条规则已经长期稳定命中,但仍在多个文档重复出现。 -- 某条规则在真实任务里持续带来误导、过度展开或错误约束。 - -## 自进化闭环 -固定按以下顺序推进: - -1. 记录信号 -- 问题现象是什么。 -- 现有哪条规则没有命中,或命中了但方向不对。 -- 这是缺能力、缺表述,还是重复定义。 - -2. 先判定变更类型 -- 新增能力:当前 skill 确实缺少某类稳定规则。 -- 修正表达:规则本身方向正确,但措辞或触发条件不清。 -- 合并重复:多份文档重复定义同一约束。 -- 退役规则:旧规则已经过时、误导或被新规则覆盖。 - -3. 只生成候选版 -- 先改出候选版,而不是宣称“skill 已自动学会”。 -- 先使用 [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) 生成提案骨架,再补全提案内容。 -- 候选改动必须同时写清: - - 改什么 - - 为什么改 - - 替代或合并哪条旧规则 - - 预期解决哪类失真 - -4. 运行验证 -- 至少执行结构校验、引用校验和场景校验。 -- 若候选改动影响输出结构、排障纪律或迁移门禁,必须补跑相关验证场景。 -- 使用 [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) 为提案写入验证记录,并把提案状态推进到 `validated` 或 `rejected`。 -- 若已经回放具体场景,使用 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 把 `通过 / 部分通过 / 不通过`、命中点、偏差点和改进建议写入同一份验证记录;当所有场景均完成且结果满足条件时,提案可自动进入 `ready_to_promote`。 -- 若提案已进入 `ready_to_promote`,使用 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看提示,再使用 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录授权并把提案推进到 `approved`。 - -5. 通过后再晋升 -- 只有候选版通过验证,才作为新的 active 版本继续使用。 -- 验证不通过时,只允许继续修正候选版,不得直接覆盖 active 版。 -- `ready_to_promote` 可以自动判定,但不自动晋升。 -- `approved` 必须通过显式授权产生,不自动推进。 -- 晋升时使用 [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) 归档当前稳定快照、更新 active 版本,并把提案状态推进到 `promoted`;该脚本要求提案状态已经是 `approved`。 -- 需要快速演示整条链路时,使用 [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh);脚本默认在结尾自动回滚到 `v1`。 - -## 候选版约束 -- 每次提案优先做最小改动,不同时重写主 skill 和大量 reference。 -- 每次提案尽量只处理一个核心问题;若同时发现多个问题,先拆成多个候选改动。 -- 若新增一条规则,必须同时回答:它替代哪条旧规则,或为什么不能复用旧规则。 -- 若两次连续提案都只是在加规则而没有合并、收紧或退役旧规则,第三次必须先做瘦身检查。 - -## 自动验证门禁 -候选版至少通过以下检查: -- `SKILL.md` frontmatter 合法。 -- `agents/openai.yaml` 结构合法。 -- `SKILL.md` 中引用的 `references/` 文件存在。 -- 主 skill 仍保持分层,不把根因纪律、输出模板、工具预算重新混写。 -- 命中的验证场景没有回归。 - -建议执行: -- 运行 [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) 做基础校验。 -- 运行 [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) 维护提案状态;允许的状态只有 `draft`、`validated`、`ready_to_promote`、`approved`、`promoted`、`rejected`。 -- 按 [validation_scenarios.md](validation_scenarios.md) 选择受影响的场景做前向验证。 -- 运行 [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) 追加结构化场景验证结论。 -- 运行 [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) 查看是否已满足授权前置条件和推荐提示。 -- 运行 [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) 记录显式授权。 -- 需要回退时,使用 [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) 恢复已归档版本。 - -## 晋升与回滚 -- 晋升原则:只有通过验证、处于 `ready_to_promote`、并已记录显式授权的候选版,才能在收到显式命令后成为新的 active 版。 -- 回滚原则:如果新规则导致输出更长、命中率下降、工具调用失控或与既有铁律冲突,应回退到上一个稳定版本。 -- 若当前任务只是在探索规则是否需要调整,可以先保留候选改动,不强制立即晋升。 - -## 明确禁止的模式 -- 因一次偶发失误就新增永久规则。 -- 新增规则时不说明替代关系。 -- 用新增规则掩盖已有规则表达不清的问题。 -- 没跑验证就宣布 skill 已学会。 -- 连续扩容规则而不做瘦身、合并或退役。 - -## 提案模板 -需要做自进化时,优先按以下模板组织变更: - -```text -问题信号 -- 真实任务里出现了什么偏差 - -变更类型 -- 新增能力 / 修正表达 / 合并重复 / 退役规则 - -变更内容 -- 修改哪些文件 -- 替代或合并哪条旧规则 - -预期收益 -- 会减少什么失真 -- 会减少什么上下文浪费 - -验证 -- 跑了哪些结构检查 -- 回放了哪些验证场景 -- 还有哪些残留风险 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_concurrency.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_concurrency.md deleted file mode 100644 index 87a4a93..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_concurrency.md +++ /dev/null @@ -1,61 +0,0 @@ -# Swift 并发架构 - -## 适用场景 -用于设计、实现和审查: -- `async/await`、`Task`、`TaskGroup` -- `@MainActor`、`actor`、`Sendable` -- 旧回调 API 迁移 -- 任务取消、状态同步、并发 Bug 排查 - -## 总原则 -- 把并发问题理解为“隔离、所有权、取消、顺序”问题,而不是“线程切换技巧”问题。 -- 必须使用结构化并发。 -- UI 状态和 UI 更新必须受 `@MainActor` 约束。 -- 必须审查跨并发域共享可变状态。 - -## 强制规则 -### Actor 与隔离 -- 共享可变状态必须放入 `actor` 或改成不可变值语义。 -- 不是所有对象都该标 `@MainActor`;只把真正 UI 相关的状态放到主隔离域。 -- 若某个类型跨域传递频繁,先评估是否设计出了错误边界。 - -### Sendable -- 跨任务、跨 Actor 传递的数据必须评估 `Sendable`。 -- 能用 `struct` / `enum` 解决时,不要用引用类型硬扛。 -- `@unchecked Sendable` 只能作为有严格内部同步保证的最后手段,必须说明理由。 - -### 任务生命周期 -- 每个任务都要能回答:谁创建、谁持有、谁取消、何时结束。 -- 使用父子任务关系传播取消。 -- 不允许到处散落无归属的 `Task {}`。 - -## 常见设计规则 -### ViewModel -- 面向 UI 的 ViewModel 标注 `@MainActor`。 -- 异步加载流程需要明确“开始加载、取消旧任务、接收结果、忽略过期结果”的规则。 -- 不要在 ViewModel 中混用多种并发模型导致状态来源不一致。 -- 搜索、流式输出、分页和快速切换场景,优先检查是否存在“旧任务结果覆盖新状态”的问题,再考虑其他并发假设。 - -### 并行任务 -- 独立子任务使用 `async let`。 -- 动态数量或聚合类任务使用 `TaskGroup`。 -- 对网络聚合、图片预取、批量加载,要明确取消和错误传播策略。 - -### 旧接口桥接 -- 使用 `withCheckedContinuation` / `withCheckedThrowingContinuation` 时,必须确保只恢复一次。 -- 桥接层只做协议适配,不顺手塞入业务逻辑。 -- 迁移期间要防止 callback 和 async 双通道同时改状态。 - -## 高风险信号 -- 在非主隔离域修改 UI 相关状态 -- 多个任务竞争写同一份可变数据 -- 任务取消后仍回写 UI -- 用 `DispatchQueue.main.async` 掩盖真正的时序问题 -- 为了通过编译随意加 `nonisolated`、`@preconcurrency`、`@unchecked Sendable` - -## 审查清单 -- [ ] UI 更新和 UI 状态发布是否明确受 `@MainActor` 保护? -- [ ] 共享可变状态是否有明确隔离策略? -- [ ] 跨域传递的类型是否满足 `Sendable` 语义? -- [ ] 任务是否具备清晰的创建、持有、取消和完成边界? -- [ ] 是否错误地用 GCD、延迟回调或无归属 `Task` 修补并发问题? diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_style.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_style.md deleted file mode 100644 index ded858b..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/swift_style.md +++ /dev/null @@ -1,50 +0,0 @@ -# Swift 编码风格 - -## 使用规则 -- 涉及命名、声明顺序、访问控制、强制解包、嵌套深度、代码结构、并发写法一致性等编码风格问题时,按本文件规则输出审查意见或代码。 -- 本文件只沉淀风格层约束;架构边界、状态归属、并发隔离、UI 布局等问题归对应专题文档。 -- 审查代码或产出代码时,若违反本文件条款,必须明确指出并给出修正方向。 - -## 属性声明与位置 -- 属性声明除非确有必要(例如必须立即初始化、纯值语义数据、并发安全要求等),否则优先使用 `lazy var` 声明。 -- 属性统一放在当前 `class` 的最下面,避免初始化分散和可见性交错。 - -## `self` 前缀 -- 变量与方法调用默认使用 `self.` 前缀。 -- 前缀不是为了消歧义而存在,而是为了让"当前作用域属性 vs 局部变量"在阅读时一目了然,避免后期新增同名变量造成隐性覆盖。 - -## 访问控制 -- 默认显式声明访问控制:优先最小可见性(例如 `private`、`private(set)`),避免不必要的对外暴露。 -- 跨模块公开成员必须显式写 `public` 或 `package`,不得用默认 `internal` 代替有意图的公开声明。 - -## 禁止崩溃类 API -- 禁止强制解包、强转与断言式崩溃(例如 `!`、`as!`、`fatalError`),除非明确写出不可变前提与失败代价。 -- 若必须崩溃,必须在代码附近注释说明"前提是什么、失败代价是什么、为什么不能走错误路径"。 - -## 嵌套深度与早退出 -- 控制嵌套深度:优先使用 `guard` 做前置条件早退出,避免多层 `if` / `switch` 嵌套。 -- 单个函数缩进层级一般不超过 3 层;超过时优先拆函数或抽取子过程,而不是继续加分支。 - -## 代码结构顺序 -- 固定代码结构顺序:`typealias` / `enum` -> 初始化 -> public API -> private helpers。 -- 协议实现放在对应 `extension` 中分组,不与主体类混写。 -- `IBOutlet` / `IBAction` 若存在,与协议 extension 一样单独分组。 - -## 命名 -- Bool 类型以 `is` / `has` / `can` 前缀,例如 `isLoading`、`hasUnreadMessages`、`canSubmit`。 -- 异步 / 并发相关方法用清晰动词短语表达意图,例如 `refreshFeed()`、`cancelInflightRequests()`,不使用 `doXxx`、`handleXxx` 这类模糊动词。 -- 避免含糊缩写:`mgr`、`ctrl`、`tmp`、`val` 在新代码中一律禁止,保留已有缩写时不扩散到新模块。 -- 禁止使用 `Snapshot`、`快照` 及同类命名,统一采用更贴近业务语义的名称(例如 `pinnedFollowUpIdentifier`、`savedDraft`、`pendingOrder`)。 - -## 并发写法一致性 -- 并发边界写清楚:UI 更新策略统一(例如 `@MainActor` 或明确切主线程),避免同一模块混用多种写法导致边界不清。 -- 选定一种写法后,同一模块内不允许 `@MainActor` 与 `DispatchQueue.main.async` / `MainActor.run {}` 等写法混用;需要切换时必须整体迁移,不得局部补丁。 -- 相关并发设计规则见 [swift_concurrency.md](swift_concurrency.md)。 - -## 常见反模式 -- 为图省事把所有属性声明为 `var`,不声明 `private(set)` 或 `let`。 -- 用 `!` 取消编译警告而不分析失败前提。 -- `guard` 被嵌套 `if` 吞没,早退出逻辑反而藏在更深的缩进里。 -- 协议实现散落在类主体内,读者无法一眼看出哪些是协议契约。 -- Bool 名称没有前缀(`loading`、`error`),读者看不出是状态标志还是值。 -- 同一个模块里同时使用 `@MainActor`、`DispatchQueue.main.async`、`MainActor.run {}`,UI 更新边界失控。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/team_collaboration.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/team_collaboration.md deleted file mode 100644 index ec0bd22..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/team_collaboration.md +++ /dev/null @@ -1,55 +0,0 @@ -# 团队协作规范 - -## 目录 -- 使用规则 -- 变更边界 -- 模块 ownership -- PR 规则 -- Review 责任 -- 技术债处理 -- 沟通与决策同步 -- 常见反模式 - -## 使用规则 -- 涉及多人协作、跨模块改动、长期重构、共享组件治理时,必须使用本文件规则。 -- 技术方案必须同时考虑代码正确性、团队协作成本和后续维护责任。 -- 不得只从“当前需求能做完”角度做局部最优决策。 -- 若当前任务没有明确的多人协作、共享模块、发布流程或 PR 上下文,本文件降级为风险提醒,不强制输出完整 ownership、PR 拆分或团队同步流程。 - -## 变更边界 -- 每次改动必须明确边界:改什么、不改什么、影响谁、由谁验证。 -- 单次 PR 必须保持主题单一,不得把功能改动、重构、样式调整、顺手修复混在一起。 -- 若确实需要跨多个模块改动,必须先写清影响面和依赖顺序。 - -## 模块 ownership -- 每个 Feature、Core 模块、共享组件都必须有明确 ownership。 -- 非 owner 修改共享模块时,必须说明改动原因、影响面和验证方式。 -- 共享模块改动必须同时考虑兼容性和下游影响。 - -## PR 规则 -- PR 标题必须说明变更目标,不得使用模糊标题。 -- PR 描述必须写清:背景、改动范围、风险、验证方式、未覆盖风险。 -- 大型改动必须拆分为多个可独立审查的 PR。 -- 架构重构 PR 必须附带决策记录或阶段计划。 - -## Review 责任 -- Review 不只是看代码风格,必须检查正确性、边界、回归风险、测试和可维护性。 -- Reviewer 必须关注共享模块、状态边界、并发边界和副作用传播。 -- 若改动会影响其他团队或其他模块,Reviewer 必须要求补充影响说明。 - -## 技术债处理 -- 技术债必须显式记录,不得口头遗留。 -- 若本次不处理技术债,必须说明原因、风险和后续处理条件。 -- 不得把临时兼容方案伪装成长期架构。 - -## 沟通与决策同步 -- 架构决策、迁移计划、兼容策略必须可被团队复用。 -- 关键结论必须沉淀为文档,而不是只存在聊天记录里。 -- 涉及跨人协作的高风险改动,必须同步回滚条件和失败预案。 - -## 常见反模式 -- 一个 PR 同时做需求、重构、性能优化、样式调整 -- 修改共享模块但不说明影响面 -- Reviewer 只看命名和格式,不看风险 -- 技术债不记录,只留“后面再说” -- 临时兼容方案长期留存 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/terminology.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/terminology.md deleted file mode 100644 index 826f11d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/terminology.md +++ /dev/null @@ -1,89 +0,0 @@ -# 中英文术语表 - -## 目录 -- 使用规则 -- 总体命名规则 -- 架构与分层术语 -- 建模术语 -- 并发术语 -- UI 与状态术语 -- 网络与数据术语 -- 工程协作术语 -- 禁止混用规则 - -## 使用规则 -- 输出方案、代码审查、排障结论、架构设计、迁移计划时,必须使用本文件统一术语。 -- 同一轮回答中,同一个概念只能使用一种主称呼。 -- 需要保留英文术语时,首次出现使用“中文主称呼 + 英文原词”格式,后续固定使用同一称呼。 - -## 总体命名规则 -- 面向中文叙述时,中文为主,英文为辅。 -- 面向 Swift 类型、协议、枚举、文件名、模块名时,保留英文命名。 -- Apple 官方框架、语言关键字、协议名、属性包装器保留英文原词。 -- 禁止中英文来回切换导致一个概念出现多个别名。 - -## 架构与分层术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 架构边界 | Architecture Boundary | 叙述分层责任时使用 | -| 依赖注入 | Dependency Injection, DI | 首次可写“依赖注入(DI)” | -| 路由协调器 | Coordinator | 类型名保留 `Coordinator`,正文可写“路由协调器(Coordinator)” | -| 用例 | UseCase | 类型名保留 `UseCase` | -| 仓储 | Repository | 类型名保留 `Repository` | -| 服务 | Service | 类型名保留 `Service` | -| 功能模块 | Feature | 叙述业务模块时使用“功能模块”,代码名保留 `Feature` | -| 核心模块 | Core | 叙述基础层时使用“核心模块”,代码名保留 `Core` | - -## 建模术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 传输模型 | DTO | 首次可写“传输模型(DTO)” | -| 领域实体 | Entity | 首次可写“领域实体(Entity)” | -| 页面状态 | ViewState | 首次可写“页面状态(ViewState)” | -| 错误模型 | ErrorModel | 首次可写“错误模型(ErrorModel)” | -| 映射层 | Mapper | 若明确存在独立层,可写“映射层(Mapper)” | - -## 并发术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 主线程隔离 | @MainActor | 叙述规则时使用 | -| Actor 隔离 | actor | 保留关键字原词 | -| 结构化并发 | Structured Concurrency | 叙述并发模型时使用 | -| 取消语义 | Cancellation | 叙述任务取消规则时使用 | -| 可发送语义 | Sendable | 首次可写“可发送语义(Sendable)” | - -## UI 与状态术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 页面状态机 | State Machine | 叙述复杂页面状态流时使用 | -| 空态 | Empty State | 叙述成功但无数据场景 | -| 错误态 | Error State | 叙述失败渲染场景 | -| 加载态 | Loading State | 叙述加载过程 | -| 列表身份 | Identity | 叙述列表稳定标识问题 | - -## 网络与数据术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 请求端点 | Endpoint | 类型名保留 `Endpoint` | -| 请求构建器 | RequestBuilder | 类型名保留 `RequestBuilder` | -| API 客户端 | APIClient | 类型名保留 `APIClient` | -| 幂等 | Idempotency | 叙述写操作安全性时使用 | -| 游标分页 | Cursor-based Pagination | 叙述游标类分页 | -| 页码分页 | Page-based Pagination | 叙述页码类分页 | -| 鉴权刷新 | Token Refresh | 叙述 Token 更新链路 | - -## 工程协作术语 -| 统一称呼 | 英文原词 | 使用规则 | -|------|------|------| -| 代码审查 | Review | 正文统一写“代码审查”,必要时首次写“代码审查(Review)” | -| 合并请求 | PR | 正文统一写“PR” | -| 模块负责人 | Owner / Ownership | 正文统一写“模块负责人”或“ownership”之一;本 skill 统一写“模块 ownership” | -| 灰度发布 | Rollout | 叙述阶段放量时使用 | -| 回滚条件 | Rollback Condition | 叙述发布失败退出条件时使用 | - -## 禁止混用规则 -- 不要把 `DTO`、`Entity`、`ViewState`、`ErrorModel` 统称为 `Model`。 -- 不要在同一段里混用“控制器”“VC”“ViewController”三种称呼。 -- 不要在同一段里混用“代码审查”“Review”“PR Review”三种称呼。 -- 不要在同一段里混用“所有权”“ownership”“owner 归属”三种称呼。 -- 不要把“页面状态”“业务状态”“组件状态”混成一个“状态”。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/test_system_prompt.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/test_system_prompt.md deleted file mode 100644 index a6eaf4d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/test_system_prompt.md +++ /dev/null @@ -1,89 +0,0 @@ -# 测试体系与自动修复 Prompt - -当用户要求构建 iOS 测试体系、补全核心业务测试、执行测试并修复失败时,按以下通用 Prompt 执行: - -```text -你是一个追求高质量代码的 iOS 测试专家,同时具备生产级 Swift / UIKit / SwiftUI / XCTest 工程能力。 - -你的目标不是“补几个测试”,而是构建可靠的测试体系,并在测试暴露缺陷后进行最小可验证修复,直到核心业务逻辑具备可上线信心。 - -项目背景: -- 这是 iOS 工程,不要使用 macOS 目标进行编译或测试。 -- 如果出现 “building for macOS” 或 macOS 相关编译失败,优先检查 scheme / destination / platform 设置。 -- 编译与测试必须使用 iPhone 模拟器或真机目标。 -- 优先使用 XCTest / XCUITest / 项目现有测试框架,不引入不必要的新依赖。 - -推荐验证命令: -1. 先查看可用 scheme: - - xcodebuild -list -workspace .xcworkspace - -2. 使用 iPhone 模拟器编译: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - build - -3. 使用 iPhone 模拟器运行测试: - - xcodebuild \ - -workspace .xcworkspace \ - -scheme \ - -configuration Debug \ - -destination 'platform=iOS Simulator,name=iPhone 16' \ - test - -如果项目只有 .xcodeproj,则把 -workspace 替换为: - - -project .xcodeproj - -核心要求: -1. 测试范围 -- 覆盖所有核心业务逻辑。 -- 优先覆盖边界条件、异常路径、空数据、网络失败、解析失败、超时、取消、状态切换、并发回调、过期结果、重复请求、缓存命中/失效、用户输入校验。 -- 不要求为了覆盖率测试纯 UI 样式、简单 getter/setter、无业务分支的样板代码。 - -2. 测试质量 -- 每个测试必须有明确断言。 -- 禁止无效测试,例如只调用方法但没有断言、只验证“不崩溃”、断言实现细节而非业务结果、为提高覆盖率而测试无意义代码、依赖真实网络/真实时间/随机结果/外部不可控状态。 -- 测试命名必须表达业务场景、输入条件和期望结果。 -- 优先使用 mock / stub / fake / dependency injection 隔离外部依赖。 - -3. 代码设计 -如果发现代码设计不利于测试,例如强耦合、直接依赖单例、直接访问真实网络/文件/时间/UserDefaults、异步生命周期不清晰、ViewModel 与 View/网络/存储混杂、状态由多个 Bool 拼接导致不可验证,允许进行最小重构,但必须说明: -- 为什么当前设计难以测试。 -- 重构边界是什么。 -- 是否改变线上行为。 -- 如何保证兼容。 -- 重构后如何提升可测试性。 - -禁止为了测试大规模重写模块。 - -4. 执行流程 -必须按以下流程循环,最多 3 轮: -- 分析:识别核心业务逻辑入口,梳理依赖关系、状态流、错误路径、异步边界,明确单测/集成测试/UI 测试边界,并给出测试计划。 -- 生成测试:新增或补全测试文件,每个测试具备 Arrange / Act / Assert 结构;异步测试设置明确 expectation / timeout;并发或取消逻辑验证过期结果不会污染当前状态。 -- 执行测试:使用 iPhone 模拟器或真机执行 build / test;不要使用 macOS destination;如果 destination 不存在,先列出可用模拟器或改用当前可用 iPhone 模拟器;记录执行命令和关键失败信息。 -- 失败分析:不要盲改,先判断失败类型是测试写错、产品代码缺陷、环境/scheme/destination 问题、异步时序问题还是依赖未隔离,并输出根因、为什么、修法、验证方式。 -- 修复:优先最小修复;不允许绕过测试、删除断言、放宽断言来让测试通过;不允许用 force unwrap / force cast / fatalError 掩盖问题;UI 或状态更新必须保证在主线程;异步任务必须明确创建者、持有者、取消时机和释放时机。 -- 回归测试:重新执行相关测试;必要时执行更大范围测试;最多循环 3 次;如果 3 次后仍失败,停止继续扩大修改,输出阻塞原因和建议。 - -5. 最终输出 -必须输出: -- 测试体系总结:新增/修改了哪些测试,覆盖了哪些核心业务逻辑、边界条件和异常路径。 -- 执行结果:build 是否通过,test 是否通过,使用的 destination、关键命令、失败测试列表。 -- 覆盖率:如果能获取覆盖率,输出整体覆盖率和关键模块覆盖率;如果无法获取覆盖率,说明原因,并给出替代判断依据。 -- 缺陷与修复:发现了哪些真实缺陷,修复了哪些问题,是否有为了可测试性进行重构,重构是否改变线上行为。 -- 风险点:未覆盖路径、仍可能存在的边界风险、环境或 CI 风险、异步/并发/状态残留风险。 -- 上线判断:是否可以上线 Yes / No,理由必须具体;如果是 No,说明上线前必须完成哪些事项。 - -工作原则: -- 以可靠性为目标,不以测试数量为目标。 -- 以真实业务断言为准,不制造虚假覆盖率。 -- 优先证明核心路径正确,再补边界与异常路径。 -- 最小改动,避免无关重构。 -- 所有结论必须来自代码分析、测试结果或明确证据。 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/testing_strategy.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/testing_strategy.md deleted file mode 100644 index 78b7306..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/testing_strategy.md +++ /dev/null @@ -1,156 +0,0 @@ -# 测试策略 - -## 目录 -- 使用规则 -- 测试策略输出模板 -- 测试层次要求 -- 场景化要求 -- 常见错误 -- 最终交付要求 - -## 使用规则 -- 提交实现方案、重构方案、修复方案时,必须同时给出测试策略。 -- 测试策略必须写清“测试什么、怎么测、覆盖到哪里、剩余风险是什么”。 -- 没有验证路径的实现,不视为可交付方案。 -- 默认只给短模板;只有命中高风险迁移、复杂并发、性能专项、发布风险或用户明确要求展开时,才追加完整模板。 -- 本文件只定义验证范围和验证方式,不重复定义根因分析、工具预算或通用答法骨架。 - -## 短模板模式 -默认先用短模板回答,必要时再追加完整模板。 - -```text -测试覆盖 -- 覆盖哪些路径 - -验证方式 -- 如何验证 - -未覆盖风险 -- 当前仍有哪些风险 -``` - -## 测试策略输出模板 -```text -测试目标 -- 这次要验证什么 - -测试范围 -- 覆盖哪些模块 -- 不覆盖哪些模块 - -测试层次 -- 单元测试 -- 集成测试 -- UI / 交互验证 -- 并发验证 -- 性能验证 - -关键用例 -1. 正常路径 -2. 边界路径 -3. 错误路径 -4. 回归路径 - -验证方式 -- 自动化测试 -- 真机手测 -- 日志 / 断点 / Instruments - -残留风险 -- 目前没有覆盖到什么 -- 这些风险为什么暂时接受 -``` - -使用约束: -- 只有在任务跨模块、跨阶段、跨平台或验证路径明显复杂时,才展开完整模板。 -- 若只是常规修复或局部实现,短模板已经足够,不要机械展开整份清单。 - -## 测试层次要求 -### 单元测试 -适用于: -- ViewModel -- UseCase -- Repository -- 状态转换 -- 错误映射 -- 数据格式转换 - -要求: -- 覆盖正常路径、边界路径、错误路径。 -- 对时间、网络、缓存、特性开关使用可替换依赖。 - -### 集成测试 -适用于: -- 模块间协作 -- 网络层与解码链路 -- 缓存写入读取 -- 导航与状态同步 - -要求: -- 验证关键调用链闭环。 -- 验证依赖注入、错误传播和回退行为。 - -### UI / 交互验证 -适用于: -- 列表、表单、导航、弹窗、空状态、加载状态 -- Dark Mode、Dynamic Type、横竖屏、无障碍 - -要求: -- 验证视觉状态、交互状态和回填状态一致。 -- 验证复用场景和身份稳定性。 - -### 并发验证 -适用于: -- `actor` 隔离 -- 任务取消 -- 多请求竞争 -- 过期结果回写 -- callback 到 async/await 迁移 - -要求: -- 必须验证取消后不回写。 -- 必须验证并发下状态不串线。 -- 必须验证主线程更新边界。 - -### 性能验证 -适用于: -- 启动优化 -- 列表滚动优化 -- 内存治理 -- 页面刷新优化 - -要求: -- 必须有优化前后对比。 -- 必须给出指标来源。 -- 必须说明是否影响正确性和体验。 - -## 场景化要求 -### Bug 修复 -- 必须提供复现路径。 -- 必须说明修复前如何失败、修复后如何通过。 -- 必须覆盖同类回归路径。 - -### 架构重构 -- 必须验证新旧行为一致。 -- 必须验证迁移阶段兼容性。 -- 必须明确哪些测试在阶段一做,哪些测试在阶段二做。 - -### 并发修复 -- 必须验证任务取消、竞态覆盖、线程隔离。 -- 必须说明是否需要真机压测或 Instruments。 - -### 性能优化 -- 必须给出基线、目标和结果。 -- 不允许只写“性能已提升”。 - -## 常见错误 -- 只写“已测试”,不写怎么测。 -- 只测正常路径,不测边界和错误路径。 -- 只跑模拟器,不验证真机关键场景。 -- 只说会补测试,不给明确补法。 -- 性能优化没有量化指标。 - -## 最终交付要求 -- 每次交付都必须包含测试范围。 -- 每次交付都必须说明未覆盖风险。 -- 每次交付都必须给出至少一种可复现验证路径。 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/ui_state_patterns.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/ui_state_patterns.md deleted file mode 100644 index 46f6e7d..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/ui_state_patterns.md +++ /dev/null @@ -1,117 +0,0 @@ -# UI 状态模式 - -## 目录 -- 使用规则 -- 状态分层 -- 页面状态机 -- 列表状态模式 -- 表单状态模式 -- 异步回写规则 -- 空态与错误态 -- 常见反模式 - -## 使用规则 -- 涉及页面状态、列表状态、表单状态、加载状态、错误状态时,必须先定义状态模型。 -- 不得使用多个布尔值拼凑复杂页面状态。 -- 不得让 View、ViewModel、Service 同时维护一份页面状态。 - -## 状态分层 -固定拆分为三层: -- 领域状态:业务是否成立、数据是否有效 -- 页面状态:页面当前处于加载、成功、失败、空态、刷新、分页哪一态 -- 组件状态:弹窗、按钮禁用、输入焦点、局部 loading - -要求: -- 页面状态由 ViewModel 统一产出。 -- 组件状态不得反向污染领域状态。 -- 列表项局部状态不得覆盖整个页面状态。 - -## 页面状态机 -推荐骨架: - -```swift -enum PageState: Equatable { - case idle - case loading - case loaded(ContentState) - case empty(EmptyState) - case failed(ViewError) -} -``` - -要求: -- `idle`、`loading`、`loaded`、`empty`、`failed` 五态必须明确。 -- 不得把空态混进失败态。 -- 不得把刷新中的成功态误建模为全屏 loading。 - -## 列表状态模式 -列表状态至少拆为: -- 首次加载状态 -- 下拉刷新状态 -- 分页加载状态 -- 空列表状态 -- 分页尾页状态 -- 局部错误提示状态 - -要求: -- 首刷失败与分页失败分开建模。 -- 下拉刷新不得清空已展示数据。 -- 分页失败不得覆盖已有列表内容。 -- 新刷新结果不得被旧分页结果覆盖。 - -推荐骨架: - -```swift -struct ListViewState: Equatable { - var items: [Item] - var phase: Phase - var pagination: PaginationState - - enum Phase: Equatable { - case idle - case loading - case loaded - case empty - case failed(ViewError) - } - - enum PaginationState: Equatable { - case idle - case loadingNextPage - case noMoreData - case failed(ViewError) - } -} -``` - -## 表单状态模式 -表单状态至少拆为: -- 输入值 -- 校验状态 -- 提交状态 -- 提交错误 -- 可交互状态 - -要求: -- 校验错误与提交错误分开建模。 -- 本地校验失败不得伪装成服务端失败。 -- 提交中状态必须禁止重复提交。 -- 表单草稿状态必须定义重置和回填规则。 - -## 异步回写规则 -- 任何异步结果回写前都必须确认任务未取消、状态未过期、页面仍然有效。 -- 页面切换、列表复用、搜索关键词变化后,旧结果不得覆盖新状态。 -- 过期结果必须丢弃,不做“尽力回写”。 - -## 空态与错误态 -- 空态表示“成功返回但无数据”。 -- 错误态表示“请求失败、解析失败、业务失败或关键状态不成立”。 -- 空态必须有空态语义,不得使用“暂无数据”覆盖所有失败场景。 -- 错误态必须提供用户动作:重试、返回、联系客服、检查网络。 - -## 常见反模式 -- `isLoading`、`hasError`、`isEmpty`、`hasData` 四个布尔值并存 -- 刷新时把列表直接清空造成闪屏 -- 分页失败后把整页切到失败态 -- 提交中仍允许重复点击按钮 -- 搜索关键词变化后旧请求结果覆盖新结果 diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/validation_scenarios.md b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/validation_scenarios.md deleted file mode 100644 index 610f750..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/references/validation_scenarios.md +++ /dev/null @@ -1,148 +0,0 @@ -# Skill 验证场景 - -## 使用规则 -- 用本文件验证 `ios-engineer` skill 是否真正做到:少带上下文、先抓根因、避免大改、补齐链路、控制工具调用。 -- 每次验证只测 1 个场景,不把多个场景混在一轮。 -- 验证结论只回答四件事:是否命中、哪里偏了、为什么偏、规则怎么补。 -- 建议使用固定场景标识:`layout`、`parameter-pass-through`、`concurrency`、`review`、`migration`、`mcp-control`。 - -## 验证目标 -- 输出是否优先给出最可能根因,而不是铺开多个大分支。 -- 输出是否保持短结构,而不是被模板和背景说明拖长。 -- 修复是否遵守最小改动原则,而不是上来重构模块。 -- 新增字段或参数时,是否补齐完整数据链路,而不是只修消费端。 -- 工具调用是否受控,是否避免重复搜索、重复读取和重复尝试。 - -## 场景 1:布局异常 -用户输入示例: -```text -消息气泡高度偶发错误,长文本会截断,先别重构,帮我找根因。 -``` - -通过标准: -- 先落到布局、复用、自适应高度链路。 -- 不直接建议重写整个消息视图。 -- 输出保持“根因 / 为什么 / 修法 / 验证”。 - -失败信号: -- 一上来给大量候选原因。 -- 没有先看复用、约束链路、异步回填。 -- 直接建议整体替换布局方案。 - -## 场景 2:参数透传链路 -用户输入示例: -```text -修一下 A 类这个方法。新增字段 currentModel,但它现在在 A 里拿不到,B 里也没有。 -``` - -通过标准: -- 识别这是完整数据链路问题。 -- 回溯真实来源、构造点、映射层和中间持有者。 -- 不只在 A 或 B 局部补变量。 - -失败信号: -- 只在消费端加属性。 -- 给默认值或传空值让当前文件先过。 -- 没有说明真实 source of truth。 - -## 场景 3:并发状态错乱 -用户输入示例: -```text -搜索页快速输入时结果会串线,帮我修,不要大改。 -``` - -通过标准: -- 先落到任务取消、过期结果回写、状态归属。 -- 优先最小修复,例如取消旧任务或丢弃过期结果。 -- 说明验证方式。 - -失败信号: -- 把问题泛化成“换一套架构”。 -- 只加 `DispatchQueue.main.async` 或延迟。 -- 不提取消链路。 - -## 场景 4:代码审查 -用户输入示例: -```text -review 这个改动,重点看有没有隐藏回归。 -``` - -通过标准: -- 先报正确性、竞态、生命周期、架构越界、测试缺口。 -- Findings 明显先于风格意见。 -- 结论简短,不做长篇教学。 - -失败信号: -- 先讲命名、格式、风格。 -- 没有按严重度排序。 -- 没提验证缺口。 - -## 场景 5:复杂迁移 -用户输入示例: -```text -准备把这个老的聊天页从 callback 迁到 async/await,给一个落地方案。 -``` - -通过标准: -- 先给四段式摘要。 -- 再按需要追加阶段计划、兼容层、回滚条件。 -- 不把迁移说成一次性替换。 - -失败信号: -- 没有阶段划分。 -- 没有兼容层和回滚。 -- 只讲终态,不讲迁移路径。 - -## 场景 6:MCP / 工具调用控制 -用户输入示例: -```text -这个线上偶发问题帮我查一下,日志很多,你自己看。 -``` - -通过标准: -- 先缩成现象、已知事实、关键缺口。 -- 工具调用围绕 1 个主方向推进。 -- 两次无新增证据后主动切方向或收敛。 - -失败信号: -- 一次性打开大量文件或大量搜索。 -- 没有预算意识。 -- 同一方向重复尝试。 - -## 记录模板 -```text -验证场景 -- 场景名称 - -是否通过 -- 通过 / 不通过 / 部分通过 - -命中点 -- 哪些规则起作用 - -偏差点 -- 哪些行为仍然失控或偏题 - -改进建议 -- 应该补哪条规则 -- 应该删哪条重复规则 -``` - -结构化记录建议字段: - -```text -scenario -- 固定场景标识 - -result -- pass / partial / fail - -hits -- 命中的规则或行为 - -deviations -- 偏差点 - -improvements -- 改进建议 -``` diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/approve_skill_promotion.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/approve_skill_promotion.sh deleted file mode 100644 index f803356..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/approve_skill_promotion.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 2 ]; then - echo "Usage: bash scripts/approve_skill_promotion.sh " - echo 'Example: bash scripts/approve_skill_promotion.sh evolution/proposals/20260403-fix.md "approved-by-user"' - exit 1 -fi - -proposal_file="$1" -approved_by="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -if [ "$proposal_status" != "ready_to_promote" ]; then - echo "Proposal is not ready_to_promote: ${proposal_status}" - exit 1 -fi - -cat > "$approval_file" </dev/null -cat "$approval_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/check_skill_promotion_readiness.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/check_skill_promotion_readiness.sh deleted file mode 100644 index 7f205ec..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/check_skill_promotion_readiness.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/check_skill_promotion_readiness.sh " - exit 1 -fi - -proposal_file="$1" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -approval_file="evolution/approvals/${proposal_id}.json" - -proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - -approval_status="missing" -if [ -f "$approval_file" ]; then - approval_status="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["status"]' "$approval_file")" -fi - -promotion_readiness="unknown" -scenario_status="unknown" -if [ -f "$record_file" ]; then - readout="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print "#{data["promotion_readiness"]}\n#{data["scenario_validation_status"]}"' "$record_file")" - promotion_readiness="$(printf '%s' "$readout" | sed -n '1p')" - scenario_status="$(printf '%s' "$readout" | sed -n '2p')" -fi - -cat <" - exit 1 -fi - -slug="$1" -timestamp="$(date '+%Y%m%d-%H%M%S')" -proposal_path="evolution/proposals/${timestamp}-${slug}.md" - -cat > "$proposal_path" < [proposal-file]" - echo "Example: bash scripts/promote_skill_evolution.sh v2 proposal:20260403-fix-root-cause evolution/proposals/20260403-fix-root-cause.md" - exit 1 -fi - -new_version="$1" -source_ref="$2" -proposal_file="${3:-}" -history_dir="evolution/history/${new_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ -e "$history_dir" ]; then - echo "Version already exists: ${new_version}" - exit 1 -fi - -if [ -n "$proposal_file" ]; then - if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 - fi - - proposal_status="$(ruby - "$proposal_file" <<'RUBY' -proposal_file = ARGV[0] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -print lines[value_index].sub(/^- /, "").strip -RUBY -)" - - if [ "$proposal_status" != "approved" ]; then - echo "Proposal is not approved: ${proposal_status}" - exit 1 - fi - - proposal_id="$(basename "$proposal_file" .md)" - approval_file="evolution/approvals/${proposal_id}.json" - if [ ! -f "$approval_file" ]; then - echo "Missing approval record: ${approval_file}" - exit 1 - fi -fi - -bash scripts/validate_skill_evolution.sh - -mkdir -p "$snapshot_dir" -cp SKILL.md "${snapshot_dir}/SKILL.md" -cp -R agents "${snapshot_dir}/agents" -cp -R references "${snapshot_dir}/references" -cp -R scripts "${snapshot_dir}/scripts" - -cat > "${history_dir}/metadata.json" < evolution/active_version.json </dev/null -fi - -echo "Promoted ${new_version}" diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/record_validation_scenario.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/record_validation_scenario.sh deleted file mode 100644 index a2038ba..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/record_validation_scenario.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 6 ]; then - echo "Usage: bash scripts/record_validation_scenario.sh " - echo 'Example: bash scripts/record_validation_scenario.sh evolution/proposals/20260403-fix.md layout pass "命中根因四段式;先看复用链路" "无" "无"' - exit 1 -fi - -proposal_file="$1" -scenario="$2" -result="$3" -hits_raw="$4" -deviations_raw="$5" -improvements_raw="$6" - -case "$result" in - pass|partial|fail) - ;; - *) - echo "Unsupported result: ${result}" - exit 1 - ;; -esac - -proposal_id="$(basename "$proposal_file" .md)" -record_file="evolution/validations/${proposal_id}.json" -lock_dir="evolution/validations/${proposal_id}.lock" - -if [ ! -f "$record_file" ]; then - echo "Missing validation record: ${record_file}" - exit 1 -fi - -for _ in 1 2 3 4 5 6 7 8 9 10; do - if mkdir "$lock_dir" 2>/dev/null; then - break - fi - sleep 0.1 -done - -if [ ! -d "$lock_dir" ]; then - echo "Failed to acquire validation record lock: ${lock_dir}" - exit 1 -fi - -cleanup() { - rmdir "$lock_dir" 2>/dev/null || true -} -trap cleanup EXIT - -ruby -rjson - "$record_file" "$scenario" "$result" "$hits_raw" "$deviations_raw" "$improvements_raw" <<'RUBY' -record_file, scenario, result, hits_raw, deviations_raw, improvements_raw = ARGV - -def split_items(text) - text.split(";").map(&:strip).reject(&:empty?) -end - -data = JSON.parse(File.read(record_file)) -records = data["scenario_records"] || [] - -entry = { - "scenario" => scenario, - "result" => result, - "hits" => split_items(hits_raw), - "deviations" => split_items(deviations_raw), - "improvements" => split_items(improvements_raw) -} - -idx = records.find_index { |item| item["scenario"] == scenario } -if idx - records[idx] = entry -else - records << entry -end - -results = records.map { |item| item["result"] } -status = - if records.empty? - "not_run" - elsif results.any? { |item| item == "pending" } - "pending" - elsif results.any? { |item| item == "fail" } - "failed" - elsif results.any? { |item| item == "partial" } - "partial" - else - "passed" - end - -data["scenario_records"] = records -data["scenario_validation_status"] = status -data["promotion_readiness"] = - if status == "passed" && data["status"] == "validated" - "ready_to_promote" - else - "not_ready" - end -data["updated_at"] = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z") - -File.write(record_file, JSON.pretty_generate(data) + "\n") -RUBY - -next_status="$(ruby -rjson -e 'data = JSON.parse(File.read(ARGV[0])); print(data["promotion_readiness"] == "ready_to_promote" ? "ready_to_promote" : data["status"])' "$record_file")" -bash scripts/update_skill_proposal_status.sh "$proposal_file" "$next_status" >/dev/null -cat "$record_file" diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/rollback_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/rollback_skill_evolution.sh deleted file mode 100644 index dadf10f..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/rollback_skill_evolution.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/rollback_skill_evolution.sh " - exit 1 -fi - -target_version="$1" -history_dir="evolution/history/${target_version}" -snapshot_dir="${history_dir}/snapshot" - -if [ ! -d "$snapshot_dir" ]; then - echo "Missing snapshot for version: ${target_version}" - exit 1 -fi - -rm -rf agents references scripts -cp "${snapshot_dir}/SKILL.md" SKILL.md -cp -R "${snapshot_dir}/agents" agents -cp -R "${snapshot_dir}/references" references -cp -R "${snapshot_dir}/scripts" scripts - -cat > evolution/active_version.json < " - exit 1 -fi - -proposal_file="$1" -new_status="$2" - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -case "$new_status" in - draft|validated|ready_to_promote|approved|promoted|rejected) - ;; - *) - echo "Unsupported status: ${new_status}" - exit 1 - ;; -esac - -ruby - "$proposal_file" "$new_status" <<'RUBY' -proposal_file = ARGV[0] -new_status = ARGV[1] -lines = File.readlines(proposal_file) -status_index = lines.find_index { |line| line.strip == "## 状态" } -abort("Missing status section") unless status_index -value_index = status_index + 1 -abort("Missing status value") unless value_index < lines.length -lines[value_index] = "- #{new_status}\n" -File.write(proposal_file, lines.join) -RUBY - -echo "Updated ${proposal_file} -> ${new_status}" diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_evolution.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_evolution.sh deleted file mode 100755 index da30f87..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_evolution.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -echo "[1/4] Validate YAML structure" -ruby -e 'require "yaml"; YAML.load_file("SKILL.md"); YAML.load_file("agents/openai.yaml"); puts "YAML OK"' - -echo "[2/4] Validate SKILL.md size" -line_count="$(wc -l < SKILL.md | tr -d ' ')" -if [ "$line_count" -gt 500 ]; then - echo "SKILL.md too long: ${line_count} lines" - exit 1 -fi -echo "SKILL.md lines: ${line_count}" - -echo "[3/4] Validate referenced files exist" -missing=0 -while IFS= read -r path; do - [ -z "$path" ] && continue - if [ ! -f "$path" ]; then - echo "Missing reference: $path" - missing=1 - fi -done < <(rg -o 'references/[A-Za-z0-9_./-]+\.md' SKILL.md | sort -u) - -if [ "$missing" -ne 0 ]; then - exit 1 -fi -echo "Reference files OK" - -echo "[4/4] Validate layering guardrails" -if rg -q '^## (调用预算|重试与限流|上下文压缩|防循环退出条件|输出要求)$' references/root_cause_enforcement.md; then - echo "root_cause_enforcement.md should not define MCP control sections" - exit 1 -fi - -if rg -q '^## (核心原则|排障标准流程|调用预算|重试与限流|防循环退出条件)$' references/examples.md; then - echo "examples.md should not define root-cause or MCP control sections" - exit 1 -fi - -echo "Layering guardrails OK" -echo "Base validation passed" diff --git a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_proposal.sh b/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_proposal.sh deleted file mode 100644 index ffeddde..0000000 --- a/skills-engineering/ios-engineer/evolution/history/v9/snapshot/scripts/validate_skill_proposal.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT_DIR" - -if [ $# -lt 1 ]; then - echo "Usage: bash scripts/validate_skill_proposal.sh [scenario-slug ...]" - echo "Example: bash scripts/validate_skill_proposal.sh evolution/proposals/20260403-fix.md layout parameter-pass-through" - exit 1 -fi - -proposal_file="$1" -shift || true - -if [ ! -f "$proposal_file" ]; then - echo "Missing proposal file: ${proposal_file}" - exit 1 -fi - -proposal_id="$(basename "$proposal_file" .md)" -timestamp="$(date '+%Y-%m-%dT%H:%M:%S%z')" -record_file="evolution/validations/${proposal_id}.json" -tmp_output="$(mktemp)" - -set +e -bash scripts/validate_skill_evolution.sh >"$tmp_output" 2>&1 -exit_code=$? -set -e - -scenario_status="not_run" -scenario_records='[]' - -if [ "$#" -gt 0 ]; then - scenario_status="pending" - scenario_records="$(printf '%s\n' "$@" | ruby -rjson -e 'items = STDIN.read.lines.map(&:strip).reject(&:empty?).map { |slug| {"scenario" => slug, "result" => "pending", "hits" => [], "deviations" => [], "improvements" => []} }; print JSON.generate(items)')" -fi - -if [ "$exit_code" -eq 0 ]; then - status="validated" -else - status="rejected" -fi - -escaped_output="$(ruby -rjson -e 'print JSON.dump(ARGF.read)' "$tmp_output")" - -cat > "$record_file" </dev/null -cat "$record_file" - -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi diff --git a/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh index fc535e9..e16e79e 100755 --- a/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh +++ b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh @@ -94,6 +94,21 @@ for v in "${sorted_dirs[@]}"; do fi done +# Pre-count: how many snapshots would be deleted +would_delete=0 +would_keep=0 +for v in "${sorted_dirs[@]}"; do + if grep -qx "$v" "$protected_file"; then + would_keep=$((would_keep + 1)) + else + would_delete=$((would_delete + 1)) + fi +done + +if ! $DRY_RUN; then + echo "将删除 ${would_delete} 个快照" >&2 +fi + echo "=== Evolution History GC ===" echo "Active version: $ACTIVE_VERSION" echo "Keep recent: $KEEP_RECENT" From 208258d4f5e7d029bc9f97abbbe933ced861a12c Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 18:09:00 +0800 Subject: [PATCH 18/30] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=20iOS=20Enginee?= =?UTF-8?q?r=20skill=20=E7=9A=84=E8=87=AA=E6=88=91=E8=BF=9B=E5=8C=96?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=20&=20=E6=96=B0=E5=A2=9E=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 gc_evolution_history.sh 脚本,优化历史记录回收逻辑 - 更新 self_evolution.md 参考文档 - 修复 append_usage_entry.sh 和 extract_usage_audit.sh 脚本 - 新增 tools/duplication/analyze_duplication.py 重复分析工具 - 新增对应测试文件 test_ios_engineer_duplication.py - 更新 .gitignore --- .gitignore | 1 + .../ios-engineer/references/self_evolution.md | 14 +- .../scripts/append_usage_entry.sh | 13 +- .../scripts/extract_usage_audit.sh | 28 +- .../scripts/gc_evolution_history.sh | 170 +++- tests/test_ios_engineer_duplication.py | 154 ++++ tools/duplication/analyze_duplication.py | 866 ++++++++++++++++++ 7 files changed, 1203 insertions(+), 43 deletions(-) create mode 100644 tests/test_ios_engineer_duplication.py create mode 100644 tools/duplication/analyze_duplication.py diff --git a/.gitignore b/.gitignore index 6c42038..77e1e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ skills-engineering/scripts/config.local.sh env/secrets.json *__pycache__*/ +.analysis_output/ .cursor/ .codex/ diff --git a/skills-engineering/ios-engineer/references/self_evolution.md b/skills-engineering/ios-engineer/references/self_evolution.md index 5b7e944..e3379b5 100644 --- a/skills-engineering/ios-engineer/references/self_evolution.md +++ b/skills-engineering/ios-engineer/references/self_evolution.md @@ -165,16 +165,22 @@ ## 进化历史 GC 策略 -`evolution/history/` 每次晋升产生全量快照,随版本积累会快速膨胀。以下策略控制目录体积: +`evolution/history/` 每次晋升产生全量快照,随版本积累会快速膨胀。`evolution/proposals/` 和 `evolution/approvals/` 同步联动清理,保持三者一致。以下策略控制目录体积: **保留规则**: - 始终保留最近 10 个版本的完整快照。 - 每 10 个版本(v10, v20, v30...)保留一个里程碑快照作为长期还原点。 - 其他版本的快照在晋升下一个版本后自动清理。 +**Proposals / Approvals 联动清理规则**: +- 每个 history 版本的 `metadata.json` 记录了 `source: "proposal:"`,关联对应的 proposal 和 approval。 +- 当一个 proposal 的**所有**关联 history 版本均被 GC 删除时,该 proposal 及其 approval 同步清理。 +- 未关联任何 history 版本的 proposal / approval(仍在草案、验证中、已审批未晋升)**始终保留**。 +- 无对应 proposal 的孤立 approval 文件(残留文件)也会被清理。 + **清理脚本**: ```bash -# 示例:仅保留最近 10 版 + 每 10 版里程碑 +# 示例:统一清理 history + proposals + approvals bash scripts/gc_evolution_history.sh ``` @@ -187,7 +193,9 @@ bash scripts/gc_evolution_history.sh - `active_version.json` 指向的当前版本快照。 - 里程碑版本快照(版本号能被 10 整除且 ≥ v10)。 - 最近 10 个版本的快照。 +- 关联到以上保留版本的 proposal / approval 文件。 +- 未关联任何 history 的进行中 proposal(WIP)始终保留。 **干运行模式**: -- `gc_evolution_history.sh --dry-run` 仅列出将被删除的目录,不实际删除。 +- `gc_evolution_history.sh --dry-run` 仅列出将被删除的内容,不实际删除。 - 首次部署建议先干运行确认列表。 diff --git a/skills-engineering/ios-engineer/scripts/append_usage_entry.sh b/skills-engineering/ios-engineer/scripts/append_usage_entry.sh index ba78216..4bd4042 100755 --- a/skills-engineering/ios-engineer/scripts/append_usage_entry.sh +++ b/skills-engineering/ios-engineer/scripts/append_usage_entry.sh @@ -71,7 +71,9 @@ if [ ! -d "$LOCK_DIR" ]; then exit 1 fi -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } +cleanup() { + rmdir "$LOCK_DIR" 2>/dev/null || true +} trap cleanup EXIT now="$(date '+%Y-%m-%dT%H:%M:%S%z')" @@ -93,17 +95,14 @@ errors << "task_type '#{task_type}' not in #{ALLOWED_TASK_TYPES.inspect}" unless errors << "outcome '#{outcome}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(outcome) errors << "evolution_signal '#{evolution_signal}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(evolution_signal) -# prompt_summary length 5-200 (chars, not bytes) ps_len = prompt_summary.length errors << "prompt_summary length must be 5-200 chars (got #{ps_len})" unless ps_len.between?(5, 200) -# Active rule_id set from rule_index.md active_ids = [] File.foreach(rule_index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m + match = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) + active_ids << match[1] if match end -active_set = active_ids.to_set rescue active_ids split = ->(raw) { raw.split(",").map(&:strip).reject(&:empty?) } @@ -125,8 +124,6 @@ unless errors.empty? exit 1 end -# Compute missed_rules = expected - hit (preserve expected order) -hit_set = hit_rules.to_set rescue hit_rules missed_rules = expected_rules.reject { |r| hit_rules.include?(r) } entry = { diff --git a/skills-engineering/ios-engineer/scripts/extract_usage_audit.sh b/skills-engineering/ios-engineer/scripts/extract_usage_audit.sh index 8e7920d..d1c88e8 100755 --- a/skills-engineering/ios-engineer/scripts/extract_usage_audit.sh +++ b/skills-engineering/ios-engineer/scripts/extract_usage_audit.sh @@ -36,26 +36,26 @@ if [ ! -d "$LOCK_DIR" ]; then exit 1 fi -cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; } +cleanup() { + rmdir "$LOCK_DIR" 2>/dev/null || true +} trap cleanup EXIT ruby -rjson - "$input" "$LEDGER_FILE" <<'RUBY' -require "set" - input_path, ledger_path = ARGV text = File.read(input_path) index_path = "references/rule_index.md" -ALLOWED_TOOLS = %w[codex claude-code cursor manual other].to_set.freeze -ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control other].to_set.freeze -ALLOWED_OUTCOMES = %w[pass partial fail].to_set.freeze -ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].to_set.freeze +ALLOWED_TOOLS = %w[codex claude-code cursor manual other].freeze +ALLOWED_TASK_TYPES = %w[layout parameter-pass-through concurrency review migration mcp-control notifications privacy persistence storekit extensions other].freeze +ALLOWED_OUTCOMES = %w[pass partial fail].freeze +ALLOWED_SIGNALS = ["none", "修正表达", "新增能力", "合并重复", "退役规则"].freeze ID_FORMAT = /\A[A-Z]+-\d{3}\z/ -active_ids = Set.new +active_ids = [] File.foreach(index_path) do |line| - m = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) - active_ids << m[1] if m + match = line.match(/\A\|\s*([A-Z]+-\d{3})\s*\|\s*active\s*\|/) + active_ids << match[1] if match end blocks = text.scan(/(.*?)<\/usage-audit>/m).map { |m| m[0] } @@ -88,10 +88,10 @@ blocks.each_with_index do |body, idx| end next if REQUIRED_KEYS.any? { |k| !data.key?(k) } - errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.to_a.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) - errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.to_a.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) - errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.to_a.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) - errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.to_a.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) + errors << "block #{block_no}: tool '#{data['tool']}' not in #{ALLOWED_TOOLS.inspect}" unless ALLOWED_TOOLS.include?(data["tool"]) + errors << "block #{block_no}: task-type '#{data['task-type']}' not in #{ALLOWED_TASK_TYPES.inspect}" unless ALLOWED_TASK_TYPES.include?(data["task-type"]) + errors << "block #{block_no}: outcome '#{data['outcome']}' not in #{ALLOWED_OUTCOMES.inspect}" unless ALLOWED_OUTCOMES.include?(data["outcome"]) + errors << "block #{block_no}: evolution-signal '#{data['evolution-signal']}' not in #{ALLOWED_SIGNALS.inspect}" unless ALLOWED_SIGNALS.include?(data["evolution-signal"]) ps = data["prompt-summary"] unless ps.length.between?(5, 200) diff --git a/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh index e16e79e..4ed3513 100755 --- a/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh +++ b/skills-engineering/ios-engineer/scripts/gc_evolution_history.sh @@ -6,6 +6,8 @@ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT_DIR" HISTORY_DIR="evolution/history" +PROPOSALS_DIR="evolution/proposals" +APPROVALS_DIR="evolution/approvals" ACTIVE_VERSION_FILE="evolution/active_version.json" KEEP_RECENT="${KEEP_RECENT:-10}" MILESTONE_INTERVAL="${MILESTONE_INTERVAL:-10}" @@ -17,10 +19,12 @@ while [ $# -gt 0 ]; do -h|--help) echo "Usage: bash scripts/gc_evolution_history.sh [--dry-run]" echo "" - echo "Clean up old evolution history snapshots, keeping:" - echo " - Most recent ${KEEP_RECENT} versions" + echo "Clean up old evolution artifacts, keeping:" + echo " - Most recent ${KEEP_RECENT} history versions" echo " - Every ${MILESTONE_INTERVAL}th version as milestones (v10, v20, ...)" echo " - Current active version (always protected)" + echo " - Proposals/approvals linked to kept history versions" + echo " - Proposals/approvals NOT linked to any history (work-in-progress)" echo "" echo "Options:" echo " --dry-run List what would be deleted without actually deleting" @@ -30,12 +34,13 @@ while [ $# -gt 0 ]; do esac done +# ── Phase 1: Determine which history versions to keep/delete ── + if [ ! -d "$HISTORY_DIR" ]; then echo "No history directory found: ${HISTORY_DIR}" exit 0 fi -# Get active version if [ -f "$ACTIVE_VERSION_FILE" ]; then ACTIVE_VERSION="$(ruby -rjson -e 'puts JSON.parse(File.read(ARGV[0]))["active_version"]' "$ACTIVE_VERSION_FILE")" else @@ -43,18 +48,16 @@ else exit 0 fi -# Collect all version dirs, extract version number for sorting +# Collect all version dirs tmpfile="$(mktemp)" trap 'rm -f "$tmpfile"' EXIT for dir in "$HISTORY_DIR"/v[0-9]*/; do [ -d "$dir" ] || continue dirname="$(basename "$dir")" - # Match versions created by promote_skill_evolution.sh: v1, v10, v73, v73-alpha, v73-hotfix if ! echo "$dirname" | grep -qE '^v[0-9]+(-[A-Za-z0-9]+)*$'; then continue fi - # Extract leading numeric portion for milestone calculation (v73-alpha → 73) num="$(echo "$dirname" | sed 's/^v//; s/-.*//' | sed 's/^0*//')" num="${num:-0}" echo "$num $dirname" >> "$tmpfile" @@ -65,18 +68,14 @@ if [ ! -s "$tmpfile" ]; then exit 0 fi -# Sort by version number descending sorted_dirs=($(sort -k1 -n -r "$tmpfile" | awk '{print $2}')) total="${#sorted_dirs[@]}" -# Mark protected versions using a temp file protected_file="$(mktemp)" trap 'rm -f "$tmpfile" "$protected_file"' EXIT -# Active version is always protected echo "$ACTIVE_VERSION" >> "$protected_file" -# Most recent KEEP_RECENT count=0 for v in "${sorted_dirs[@]}"; do if [ "$count" -lt "$KEEP_RECENT" ]; then @@ -85,7 +84,6 @@ for v in "${sorted_dirs[@]}"; do count=$((count + 1)) done -# Milestones (v10, v20, ...) — use only the leading numeric portion for suffixed versions for v in "${sorted_dirs[@]}"; do num="$(echo "$v" | sed 's/^v//; s/-.*//' | sed 's/^0*//')" num="${num:-0}" @@ -94,7 +92,146 @@ for v in "${sorted_dirs[@]}"; do fi done -# Pre-count: how many snapshots would be deleted +# ── Phase 2: Map proposals → history versions, determine which proposals to clean ── + +export GC_ROOT_DIR="$ROOT_DIR" +export GC_PROTECTED_FILE="$protected_file" +export GC_DRY_RUN="$DRY_RUN" + +ruby <<'RUBY' +require 'json' +require 'set' + +ROOT_DIR = ENV['GC_ROOT_DIR'] +PROTECTED_FILE = ENV['GC_PROTECTED_FILE'] +DRY_RUN = ENV['GC_DRY_RUN'] == "true" + +HISTORY_DIR = File.join(ROOT_DIR, "evolution/history") +PROPOSALS_DIR = File.join(ROOT_DIR, "evolution/proposals") +APPROVALS_DIR = File.join(ROOT_DIR, "evolution/approvals") + +# Load protected history versions +protected_versions = if File.exist?(PROTECTED_FILE) + File.readlines(PROTECTED_FILE).map(&:strip).reject(&:empty?).to_set +else + Set.new +end + +# Collect all history versions (kept and deleted) +all_history_versions = [] +Dir.glob(File.join(HISTORY_DIR, "v*/")).sort.each do |dir| + name = File.basename(dir) + next unless name.match?(/^v\d+(-[A-Za-z0-9]+)*$/) + all_history_versions << name +end + +# Build map: proposal_slug → set of history versions that reference it +proposal_to_versions = Hash.new { |h, k| h[k] = Set.new } +all_history_versions.each do |ver| + meta_file = File.join(HISTORY_DIR, ver, "metadata.json") + next unless File.exist?(meta_file) + begin + meta = JSON.parse(File.read(meta_file)) + source = meta["source"] + next unless source && source.start_with?("proposal:") + slug = source.sub(/\Aproposal:/, "") + proposal_to_versions[slug] << ver + rescue JSON::ParserError + # Skip malformed metadata + end +end + +# Determine which proposals/approvals to keep vs delete +# Rule: Keep if ANY linked history version is kept, OR if not linked to any history (WIP) +proposals_to_delete = [] +proposals_to_keep = [] +approvals_to_delete = [] +approvals_to_keep = [] +orphan_proposals = [] # not linked to any history → always keep + +Dir.glob(File.join(PROPOSALS_DIR, "*.md")).sort.each do |file| + slug = File.basename(file, ".md") + + if proposal_to_versions.key?(slug) + # Linked to history versions — check if ALL linked versions are being deleted + linked_versions = proposal_to_versions[slug] + all_deleted = linked_versions.all? { |v| !protected_versions.include?(v) } + if all_deleted + proposals_to_delete << slug + approvals_to_delete << slug + else + proposals_to_keep << slug + approvals_to_keep << slug + end + else + # Not linked to any history — work-in-progress, always keep + orphan_proposals << slug + proposals_to_keep << slug + # Check if approval exists + approval_file = File.join(APPROVALS_DIR, "#{slug}.json") + if File.exist?(approval_file) + approvals_to_keep << slug + end + end +end + +# Also find approvals without corresponding proposals (stale orphans) +Dir.glob(File.join(APPROVALS_DIR, "*.json")).sort.each do |file| + slug = File.basename(file, ".json") + proposal_file = File.join(PROPOSALS_DIR, "#{slug}.md") + unless File.exist?(proposal_file) + approvals_to_delete << slug unless approvals_to_delete.include?(slug) + end +end + +# ── Output report ── + +puts "" +puts "=== Proposals & Approvals GC ===" +puts "Linked proposals (to any history): #{proposal_to_versions.size}" +puts "Orphan proposals (no history link, WIP): #{orphan_proposals.size}" +puts "Proposals to keep: #{proposals_to_keep.uniq.size}" +puts "Proposals to delete: #{proposals_to_delete.uniq.size}" +puts "Approvals to keep: #{approvals_to_keep.uniq.size}" +puts "Approvals to delete: #{approvals_to_delete.uniq.size}" +puts "" + +if proposals_to_delete.empty? && approvals_to_delete.empty? + puts "No orphan proposals or approvals to clean up." +else + proposals_to_delete.uniq.each do |slug| + file = File.join(PROPOSALS_DIR, "#{slug}.md") + # Show which deleted versions reference it + versions = proposal_to_versions[slug].to_a.sort_by { |v| v.sub(/^v/, "").to_i } + ver_list = versions.map { |v| "#{v}(deleted)" }.join(", ") + if DRY_RUN + puts " [WOULD DELETE PROPOSAL] #{file} (linked to: #{ver_list})" + else + puts " [DELETE PROPOSAL] #{file} (linked to: #{ver_list})" + File.unlink(file) if File.exist?(file) + end + end + + approvals_to_delete.uniq.each do |slug| + file = File.join(APPROVALS_DIR, "#{slug}.json") + if DRY_RUN + puts " [WOULD DELETE APPROVAL] #{file}" + else + puts " [DELETE APPROVAL] #{file}" + File.unlink(file) if File.exist?(file) + end + end +end + +# Output list of kept orphans (for transparency) +unless orphan_proposals.empty? + puts "" + puts "WIP proposals (no history yet, always kept): #{orphan_proposals.size}" +end +RUBY + +# ── Phase 3: History snapshot GC (must run AFTER ruby because ruby reads metadata.json) ── + would_delete=0 would_keep=0 for v in "${sorted_dirs[@]}"; do @@ -105,10 +242,7 @@ for v in "${sorted_dirs[@]}"; do fi done -if ! $DRY_RUN; then - echo "将删除 ${would_delete} 个快照" >&2 -fi - +echo "" echo "=== Evolution History GC ===" echo "Active version: $ACTIVE_VERSION" echo "Keep recent: $KEEP_RECENT" @@ -132,7 +266,7 @@ done echo "" if $DRY_RUN; then - echo "DRY RUN: Would delete $deleted version(s), keep $kept version(s)" + echo "DRY RUN: Would delete $deleted history version(s), keep $kept history version(s)" else - echo "Done: Deleted $deleted version(s), kept $kept version(s)" + echo "Done: Deleted $deleted history version(s), kept $kept history version(s)" fi diff --git a/tests/test_ios_engineer_duplication.py b/tests/test_ios_engineer_duplication.py new file mode 100644 index 0000000..7831620 --- /dev/null +++ b/tests/test_ios_engineer_duplication.py @@ -0,0 +1,154 @@ +import importlib.util +import os +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace + + +REPO_ROOT = Path(__file__).resolve().parents[1] +TOOL_PATH = REPO_ROOT / "tools" / "duplication" / "analyze_duplication.py" + + +def load_duplication_tool(): + spec = importlib.util.spec_from_file_location("ios_engineer_analyze_duplication", TOOL_PATH) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +dup = load_duplication_tool() + + +class IosEngineerDuplicationTests(unittest.TestCase): + def test_extract_shell_functions_normalizes_equivalent_bodies(self) -> None: + text_a = """ +lock_one() { + for _ in 1 2 3; do + if mkdir "$LOCK_DIR" 2>/dev/null; then break; fi + sleep 0.1 + done + trap cleanup EXIT +} +""" + text_b = """ +lock_two() { + for attempt in 1 2 3; do + if mkdir "${other_lock}" 2>/dev/null; then break; fi + sleep 0.1 + done + trap cleanup EXIT +} +""" + + funcs_a = dup.extract_shell_functions(text_a) + funcs_b = dup.extract_shell_functions(text_b) + + self.assertEqual(funcs_a[0].name, "lock_one") + self.assertEqual(funcs_b[0].name, "lock_two") + self.assertEqual(funcs_a[0].normalized_body, funcs_b[0].normalized_body) + + def test_repeated_blocks_detects_cross_file_copied_logic(self) -> None: + block = """ +if [ -z "$input" ]; then + echo "missing input" + exit 1 +fi +mkdir -p "$output" +printf "%s\\n" "$input" > "$output/file.txt" +""" + fps = [ + SimpleNamespace(rel="scripts/a.sh", ext=".sh", content=f"a() {{\n{block}\n}}"), + SimpleNamespace(rel="scripts/b.sh", ext=".sh", content=f"b() {{\n{block}\n}}"), + SimpleNamespace(rel="scripts/c.sh", ext=".sh", content="echo unrelated"), + ] + + repeated = dup.extract_repeated_blocks(fps, block_size=4) + + self.assertTrue(repeated) + self.assertEqual(repeated[0]["files"], ["scripts/a.sh", "scripts/b.sh"]) + + def test_representative_blocks_collapse_same_file_set(self) -> None: + repeated = [ + {"files": ["a.sh", "b.sh"], "occurrences": [], "lines": 8, "sample": ()}, + {"files": ["a.sh", "b.sh"], "occurrences": [], "lines": 8, "sample": ()}, + {"files": ["a.sh", "c.sh"], "occurrences": [], "lines": 8, "sample": ()}, + ] + + selected = dup.select_representative_blocks(repeated) + + self.assertEqual(len(selected), 2) + self.assertEqual(selected[0]["files"], ["a.sh", "b.sh"]) + self.assertEqual(selected[1]["files"], ["a.sh", "c.sh"]) + + def test_usage_function_is_noisy_name(self) -> None: + self.assertIn("usage", dup.NOISY_FUNCTION_NAMES) + + def test_markdown_paragraphs_ignore_tables_and_code_blocks(self) -> None: + text = """ +# Title + +This paragraph is intentionally long enough to be considered for duplication detection because it describes a real rule body and not just a heading. + +| A | B | +|---|---| +| 1 | 2 | + +```bash +This code block is intentionally long enough but should be ignored by paragraph extraction. +``` +""" + + paragraphs = dup.extract_markdown_paragraphs(text) + + self.assertEqual(len(paragraphs), 1) + self.assertIn("intentionally long enough", paragraphs[0][1]) + + def test_root_entrypoint_analyzes_arbitrary_directory_without_modifying_source(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + target = root / "target" + output = root / "reports" + target.mkdir() + source = target / "sample.sh" + original = """#!/usr/bin/env bash +first() { + echo "hello" +} +""" + source.write_text(original, encoding="utf-8") + + report_path = dup.main([str(target), "--output-dir", str(output)]) + + self.assertEqual(source.read_text(encoding="utf-8"), original) + self.assertTrue(Path(report_path).exists()) + self.assertFalse((output / "all_fingerprints.json").exists()) + self.assertFalse((target / ".analysis_output").exists()) + + def test_default_output_uses_cwd_analysis_dir_named_after_target(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + target = root / "target-name" + target.mkdir() + source = target / "sample.md" + original = "A short markdown file.\n" + source.write_text(original, encoding="utf-8") + + old_cwd = Path.cwd() + try: + os.chdir(root) + report_path = Path(dup.main([str(target)])) + finally: + os.chdir(old_cwd) + + expected_dir = root / ".analysis_output" / "target-name" + self.assertEqual(report_path.parent.resolve(), expected_dir.resolve()) + self.assertTrue(report_path.exists()) + self.assertEqual(source.read_text(encoding="utf-8"), original) + self.assertFalse((target / ".analysis_output").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/duplication/analyze_duplication.py b/tools/duplication/analyze_duplication.py new file mode 100644 index 0000000..c35b28f --- /dev/null +++ b/tools/duplication/analyze_duplication.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python3 +""" +ios-engineer 去重分析工具 +步骤1: 对每个文件提取逻辑指纹 +步骤2: 汇总后对比相似度,定位重复逻辑 +""" + +import os +import re +import json +import hashlib +import argparse +from collections import defaultdict +from dataclasses import dataclass +from datetime import datetime + +DEFAULT_ROOT = os.getcwd() +ROOT = DEFAULT_ROOT +OUTPUT = os.path.join(os.getcwd(), ".analysis_output") + +# ---- 配置 ---- +SKIP_DIRS = { + "evolution/history", "evolution/proposals", "evolution/approvals", + "evolution/validations", "evolution/scenarios", ".analysis_output", + ".git", ".hg", ".svn", ".venv", "venv", "node_modules", "__pycache__", + ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "build" +} +NOISY_FUNCTION_NAMES = {"usage"} +MIN_FUNCTION_BODY_LINES = 4 +MIN_BLOCK_LINES = 8 +MIN_MARKDOWN_PARAGRAPH_CHARS = 120 + +# ---- 工具函数 ---- + +def relpath(p): + return os.path.relpath(p, ROOT) + +def directory_label(path): + label = os.path.basename(os.path.abspath(path)) or "root" + return re.sub(r'[^A-Za-z0-9_.-]+', '_', label).strip('_') or "target" + +def configure_paths(target_root, output_dir=None): + """Set the analysis root and output directory for one run.""" + global ROOT, OUTPUT + ROOT = os.path.abspath(target_root) + OUTPUT = ( + os.path.abspath(output_dir) + if output_dir + else os.path.join(os.getcwd(), ".analysis_output", directory_label(ROOT)) + ) + os.makedirs(OUTPUT, exist_ok=True) + return ROOT, OUTPUT + +def should_skip(path): + norm_parts = os.path.normpath(path).split(os.sep) + for d in SKIP_DIRS: + d_parts = d.split('/') + n = len(d_parts) + for i in range(len(norm_parts) - n + 1): + if norm_parts[i:i + n] == d_parts: + return True + return False + +def sha256(content): + return hashlib.sha256(content.encode()).hexdigest() + +def extract_ids(text): + """提取 IR-001, ROUTE-001 等规则ID""" + return set(re.findall(r'\b[A-Z]{2,6}-\d{3,4}\b', text)) + +def extract_functions(text): + """提取 shell 函数名""" + funcs = re.findall(r'^\s*(?:function\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)', text, re.MULTILINE) + return set(funcs) + +@dataclass(frozen=True) +class ShellFunction: + name: str + start_line: int + end_line: int + body: str + normalized_body: str + + @property + def body_hash(self): + return sha256(self.normalized_body) + + @property + def body_lines(self): + return len([line for line in self.normalized_body.split('\n') if line]) + +def normalize_shell_line(line, generalize=False): + """归一化 shell 逻辑行,保留命令结构但降低变量名和字符串造成的噪声。""" + stripped = line.strip() + if not stripped or stripped.startswith('#'): + return "" + stripped = re.sub(r'\s+#.*$', '', stripped) + if generalize: + stripped = re.sub(r'"(?:\\.|[^"\\])*"', '"STR"', stripped) + stripped = re.sub(r"'(?:\\.|[^'\\])*'", "'STR'", stripped) + stripped = re.sub(r'\$\{?[A-Za-z_][A-Za-z0-9_]*\}?', '$VAR', stripped) + stripped = re.sub(r'\b[A-Za-z_][A-Za-z0-9_]*=', 'VAR=', stripped) + stripped = re.sub(r'\bfor\s+[A-Za-z_][A-Za-z0-9_]*\s+in\b', 'for VAR in', stripped) + stripped = re.sub(r'\s+', ' ', stripped) + return stripped + +def normalize_shell_logic(text): + lines = [normalize_shell_line(line, generalize=True) for line in text.split('\n')] + return '\n'.join(line for line in lines if line) + +def extract_shell_functions(text): + """提取 shell 函数体,用于比较函数内部逻辑而不是只比较函数名。""" + lines = text.splitlines() + functions = [] + i = 0 + header_re = re.compile(r'^\s*(?:function\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)\s*\{?\s*$') + + while i < len(lines): + match = header_re.match(lines[i]) + if not match: + i += 1 + continue + + name = match.group(1) + start = i + body_start = i + 1 + brace_depth = lines[i].count('{') - lines[i].count('}') + + if brace_depth == 0: + while body_start < len(lines) and not lines[body_start].strip(): + body_start += 1 + if body_start < len(lines) and lines[body_start].strip() == "{": + brace_depth = 1 + body_start += 1 + else: + i += 1 + continue + + j = body_start + while j < len(lines): + brace_depth += lines[j].count('{') - lines[j].count('}') + if brace_depth <= 0: + body = '\n'.join(lines[body_start:j]) + normalized = normalize_shell_logic(body) + functions.append(ShellFunction(name, start + 1, j + 1, body, normalized)) + break + j += 1 + i = max(j + 1, i + 1) + + return functions + +def extract_sections(text): + """提取 markdown 章节标题""" + sections = re.findall(r'^#{1,4}\s+(.+)', text, re.MULTILINE) + return set(s.strip().lower() for s in sections) + +def extract_shell_patterns(text): + """提取区分度高的 shell 组合模式(排除过于通用的)""" + # 更具体的模式:命令+标志组合、特定工具调用 + patterns = [ + r'jq\s+(-r\s+)?\.', # jq 数据提取 + r'curl\s+-[sS]', # curl 静默请求 + r'find\s+.*-newer', # find 按时间 + r'sed\s+-i', # sed 原地修改 + r'grep\s+-[qci]', # grep 静默/计数/忽略大小写 + r'mktemp\s+-d', # 临时目录 + r'readonly\s+\w+', # 只读变量 + r'trap\s+', # 信号捕获 + r'set\s+-[eu]', # 安全检查 + r'pushd\s+|popd', # 目录栈 + r'diff\s+-[ru]', # diff 对比 + r'git\s+log\s+', # git log + r'git\s+diff\s+', # git diff + r'git\s+show\s+', # git show + r'date\s+\+%', # 日期格式化 + r'basename\s+\$\{?', # basename + r'dirname\s+\$\{?', # dirname + r'read\s+-r\s+', # read -r 安全读取 + r'sort\s+-[tnk]', # sort 字段排序 + r'uniq\s+-[cd]', # uniq 计数/重复 + r'cut\s+-d[= ]+-f', # cut 分隔取列 + r'tee\s+-a', # tee 追加 + r'shopt\s+-s', # bash 选项 + r'source\s+|\.\s+\$\{?', # source/import + r'python3\s+-c\s+', # python 内联 + ] + # 用模式索引而非匹配文本,确保同类模式跨文件可正确命中 + found = set() + for i, p in enumerate(patterns): + if re.search(p, text): + found.add(i) + return found + +def jaccard(set1, set2): + if not set1 and not set2: + return 0.0 + inter = len(set1 & set2) + union = len(set1 | set2) + return round(inter / union * 100, 1) if union > 0 else 0.0 + +def normalize_text(text): + """归一化文本:去注释、去空行、去首尾空白(保留行顺序)""" + lines = [] + for line in text.split('\n'): + stripped = line.strip() + if stripped and not stripped.startswith('#'): + lines.append(stripped) + return '\n'.join(lines) + +def normalize_markdown_paragraph(text): + text = re.sub(r'`[^`]+`', '`CODE`', text) + text = re.sub(r'\b[A-Z]{2,6}-\d{3,4}\b', 'RULE-ID', text) + text = re.sub(r'\s+', ' ', text.strip().lower()) + return text + +def extract_markdown_paragraphs(text): + paragraphs = [] + current = [] + start_line = 1 + in_fence = False + + for lineno, line in enumerate(text.splitlines(), start=1): + stripped = line.strip() + if stripped.startswith("```"): + in_fence = not in_fence + if current: + paragraphs.append((start_line, '\n'.join(current))) + current = [] + continue + if in_fence: + continue + if not stripped or stripped.startswith('#') or stripped.startswith('|'): + if current: + paragraphs.append((start_line, '\n'.join(current))) + current = [] + continue + if not current: + start_line = lineno + current.append(stripped) + + if current: + paragraphs.append((start_line, '\n'.join(current))) + + return [ + (line, para, normalize_markdown_paragraph(para)) + for line, para in paragraphs + if len(normalize_markdown_paragraph(para)) >= MIN_MARKDOWN_PARAGRAPH_CHARS + ] + +def extract_repeated_blocks(fps, block_size=MIN_BLOCK_LINES): + """查找跨文件重复的连续逻辑块,报告完整匹配而不是单行巧合。""" + block_index = defaultdict(list) + + for fp in fps: + if fp.ext != '.sh': + continue + norm_lines = [line for line in (normalize_shell_line(l) for l in fp.content.splitlines()) if line] + if len(norm_lines) < block_size: + continue + for idx in range(0, len(norm_lines) - block_size + 1): + block = tuple(norm_lines[idx:idx + block_size]) + block_index[sha256('\n'.join(block))].append((fp.rel, idx + 1, block)) + + repeated = [] + for _hash, occurrences in block_index.items(): + files = {rel for rel, _line, _block in occurrences} + if len(files) < 2: + continue + sample = occurrences[0][2] + repeated.append({ + "files": sorted(files), + "occurrences": occurrences, + "lines": len(sample), + "sample": sample, + }) + + repeated.sort(key=lambda item: (-len(item["files"]), -len(item["occurrences"]), item["files"])) + return repeated + +def select_representative_blocks(repeated_blocks, max_per_file_set=1): + selected = [] + counts = defaultdict(int) + for item in repeated_blocks: + key = tuple(item["files"]) + if counts[key] >= max_per_file_set: + continue + selected.append(item) + counts[key] += 1 + return selected + +# ---- 步骤1: 提取每个文件的指纹 ---- + +class FileFingerprint: + def __init__(self, path): + self.path = path + self.rel = relpath(path) + self.ext = os.path.splitext(path)[1] + self.size = os.path.getsize(path) + self.mtime = os.path.getmtime(path) + + with open(path, 'r', encoding='utf-8', errors='replace') as f: + self.content = f.read() + + self.lines = self.content.count('\n') + 1 + self.ids = extract_ids(self.content) + self.functions = extract_functions(self.content) + self.shell_functions = extract_shell_functions(self.content) if self.ext == '.sh' else [] + self.sections = extract_sections(self.content) + self.shell_patterns = extract_shell_patterns(self.content) + + # 归一化内容哈希(用于检测完全相同) + self.normalized = normalize_text(self.content) + self.norm_hash = sha256(self.normalized) + + def to_dict(self): + return { + "rel": self.rel, + "ext": self.ext, + "size": self.size, + "lines": self.lines, + "ids": sorted(self.ids), + "functions": sorted(self.functions), + "function_bodies": [ + { + "name": func.name, + "start_line": func.start_line, + "end_line": func.end_line, + "body_lines": func.body_lines, + "body_hash": func.body_hash[:16], + } + for func in self.shell_functions + ], + "sections": sorted(self.sections), + "shell_patterns": sorted(self.shell_patterns), + "norm_hash": self.norm_hash[:16] + } + + +def step1_collect(target_dir, label): + """收集目标目录内所有文件的指纹""" + fingerprints = [] + count = 0 + for root, dirs, files in os.walk(target_dir): + # 过滤不需要的目录 + dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d))] + for fname in files: + if fname.startswith('.') or fname == "analyze_duplication.sh": + continue + if fname.endswith(('.md', '.sh')): + fpath = os.path.join(root, fname) + if should_skip(fpath): + continue + try: + fp = FileFingerprint(fpath) + fingerprints.append(fp) + count += 1 + except Exception as e: + print(f" [SKIP] {relpath(fpath)}: {e}") + + # 保存指纹 + safe_label = re.sub(r'[^A-Za-z0-9_.-]+', '_', label).strip('_') or "target" + out_file = os.path.join(OUTPUT, f"{safe_label}_fingerprints.json") + with open(out_file, 'w', encoding='utf-8') as f: + json.dump([fp.to_dict() for fp in fingerprints], f, indent=2, ensure_ascii=False) + print(f" [OK] {label}: {count} 个文件 → {out_file}") + return fingerprints + +def collect_target_fingerprints(target_root): + """Collect all active markdown and shell files under the configured root.""" + fingerprints = [] + for root, dirs, files in os.walk(target_root): + dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d))] + for fname in files: + if fname.startswith('.') or fname == "analyze_duplication.sh": + continue + if not fname.endswith(('.md', '.sh')): + continue + fpath = os.path.join(root, fname) + if should_skip(fpath): + continue + try: + fingerprints.append(FileFingerprint(fpath)) + except Exception as e: + print(f" [SKIP] {relpath(fpath)}: {e}") + return fingerprints + + +# ---- 步骤2: 对比去重 ---- + +def step2_analyze(all_fps, report_path): + """对比分析所有指纹,生成报告""" + lines = [] + ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + lines.append(f"# 去重分析报告") + lines.append(f"# 生成时间: {ts}") + lines.append(f"# 目标目录: {ROOT}") + lines.append(f"# 分析范围: {len(all_fps)} 个活跃文件") + lines.append("") + + sh_fps = [fp for fp in all_fps if fp.ext == '.sh'] + md_fps = [fp for fp in all_fps if fp.ext == '.md'] + + # ======================================== + # 一、完全相同文件检测(基于内容哈希) + # ======================================== + lines.append("## 一、完全相同文件检测 (内容哈希)") + lines.append("") + + hash_groups = defaultdict(list) + for fp in all_fps: + hash_groups[fp.norm_hash].append(fp) + + dups_found = 0 + for h, group in hash_groups.items(): + if len(group) > 1: + dups_found += 1 + lines.append(f"### 重复组 #{dups_found} (哈希: `{h[:16]}...`)") + for fp in group: + lines.append(f"- `{fp.rel}` ({fp.lines} 行, {fp.size:,} bytes)") + lines.append("") + + if dups_found == 0: + lines.append("✅ 未发现完全相同的文件。") + else: + lines.append(f"⚠️ 发现 {dups_found} 组完全相同文件。") + lines.append("") + + # ======================================== + # 二、Shell 脚本重复函数检测 + # ======================================== + lines.append("## 二、Shell 脚本重复函数检测") + lines.append("") + + func_files = defaultdict(list) + for fp in sh_fps: + for func in fp.functions: + func_files[func].append(fp.rel) + + dups = { + k: v for k, v in func_files.items() + if len(v) > 1 and k not in NOISY_FUNCTION_NAMES + } + noisy_dups = { + k: v for k, v in func_files.items() + if len(v) > 1 and k in NOISY_FUNCTION_NAMES + } + if dups: + lines.append("| 函数名 | 出现文件 | 次数 |") + lines.append("|--------|----------|------|") + for func in sorted(dups.keys()): + files = ", ".join(dups[func]) + lines.append(f"| `{func}()` | {files} | {len(dups[func])} |") + lines.append("") + lines.append(f"共 {len(dups)} 个函数在多个脚本中重复定义。") + lines.append("") + lines.append("💡 **建议**: 考虑抽取到公共 helper,或保留在调用脚本内并加交叉测试。") + else: + lines.append("✅ 未发现需要审查的重复函数名。") + if noisy_dups: + lines.append("") + lines.append("以下常见 CLI 辅助函数已降噪,不作为抽公共库建议:") + for func in sorted(noisy_dups.keys()): + lines.append(f"- `{func}()` → {', '.join(noisy_dups[func])}") + lines.append("") + + # ======================================== + # 三、Shell 函数体重复检测 + # ======================================== + lines.append("## 三、Shell 函数体重复检测 (归一化后)") + lines.append("") + lines.append("比较函数体内容,忽略注释、空行、变量名和字符串字面量差异;用于发现同逻辑不同函数名。") + lines.append("") + + body_groups = defaultdict(list) + for fp in sh_fps: + for func in fp.shell_functions: + if func.name in NOISY_FUNCTION_NAMES or func.body_lines < MIN_FUNCTION_BODY_LINES: + continue + body_groups[func.body_hash].append((fp, func)) + + function_body_dups = [ + group for group in body_groups.values() + if len({fp.rel for fp, _func in group}) > 1 + ] + function_body_dups.sort(key=lambda group: (-len(group), group[0][0].rel)) + + if function_body_dups: + lines.append("| 函数体 | 出现位置 | 行数 |") + lines.append("|--------|----------|------|") + for index, group in enumerate(function_body_dups[:30], start=1): + locations = ", ".join( + f"`{fp.rel}:{func.start_line}` `{func.name}()`" + for fp, func in group + ) + lines.append(f"| 重复函数体 #{index} | {locations} | {group[0][1].body_lines} |") + lines.append("") + lines.append(f"共 {len(function_body_dups)} 组函数体重复候选。") + else: + lines.append("✅ 未发现归一化函数体重复。") + lines.append("") + + # ======================================== + # 四、Shell 连续代码块重复检测 + # ======================================== + lines.append(f"## 四、Shell 连续代码块重复检测 (≥{MIN_BLOCK_LINES} 行)") + lines.append("") + lines.append("比较跨文件连续逻辑块,适合发现锁、校验、参数解析等被复制的片段。") + lines.append("") + + repeated_blocks = extract_repeated_blocks(sh_fps, MIN_BLOCK_LINES) + representative_blocks = select_representative_blocks(repeated_blocks) + if representative_blocks: + lines.append("| 重复块 | 出现文件 | 示例 |") + lines.append("|--------|----------|------|") + for idx, item in enumerate(representative_blocks[:30], start=1): + files = ", ".join(f"`{file}`" for file in item["files"]) + sample = "
".join(item["sample"][:3]) + lines.append(f"| 代码块 #{idx} ({item['lines']} 行) | {files} | `{sample}` |") + lines.append("") + lines.append(f"共 {len(repeated_blocks)} 组原始连续代码块候选,展示 {len(representative_blocks)} 组按文件集合去重后的代表候选。") + else: + lines.append("✅ 未发现跨文件连续代码块重复。") + lines.append("") + + # ======================================== + # 五、Shell 脚本相似度矩阵 + # ======================================== + lines.append("## 五、Shell 脚本特定模式相似度 (候选信号,≥50%,≥3个模式)") + lines.append("") + lines.append("基于高区分度的 shell 模式(jq/curl/git/diff/trap 等),至少匹配3个模式才比较") + lines.append("") + + sh_pairs = [] + for i in range(len(sh_fps)): + for j in range(i + 1, len(sh_fps)): + fp1, fp2 = sh_fps[i], sh_fps[j] + # 基于函数名 + shell模式 的综合相似度 + f1 = fp1.functions | fp1.shell_patterns + f2 = fp2.functions | fp2.shell_patterns + # 至少一方有 ≥3 个模式才比较,避免"都只有set -eu"这种误报 + if len(f1) < 3 and len(f2) < 3: + continue + sim = jaccard(f1, f2) + if sim >= 50: + sh_pairs.append((sim, fp1, fp2)) + + sh_pairs.sort(key=lambda x: x[0], reverse=True) + + if sh_pairs: + lines.append("| 文件A | 文件B | 相似度 |") + lines.append("|-------|-------|--------|") + for sim, fp1, fp2 in sh_pairs[:50]: # Top 50 + lines.append(f"| `{fp1.rel}` | `{fp2.rel}` | **{sim}%** |") + lines.append("") + lines.append(f"共 {len(sh_pairs)} 对脚本特定模式相似度 ≥ 50%。") + else: + lines.append("✅ 未发现指纹相似度 ≥ 50% 的脚本对。") + lines.append("") + + # ======================================== + # 六、Shell 脚本内容相似度 + # ======================================== + lines.append("## 六、Shell 脚本内容行相似度 (≥60%)") + lines.append("") + + # 基于归一化行集合的 Jaccard + content_pairs = [] + for i in range(len(sh_fps)): + for j in range(i + 1, len(sh_fps)): + fp1, fp2 = sh_fps[i], sh_fps[j] + lines1 = set(fp1.normalized.split('\n')) + lines2 = set(fp2.normalized.split('\n')) + sim = jaccard(lines1, lines2) + if sim >= 60: + content_pairs.append((sim, fp1, fp2)) + + content_pairs.sort(key=lambda x: x[0], reverse=True) + + if content_pairs: + lines.append("| 文件A | 文件B | 行相似度 |") + lines.append("|-------|-------|----------|") + for sim, fp1, fp2 in content_pairs[:30]: + lines.append(f"| `{fp1.rel}` | `{fp2.rel}` | **{sim}%** |") + lines.append("") + lines.append(f"共 {len(content_pairs)} 对脚本内容行相似度 ≥ 60%。") + else: + lines.append("✅ 未发现内容行相似度 ≥ 60% 的脚本对。") + lines.append("") + + # ======================================== + # 七、脚本工具链指纹对比 + # ======================================== + lines.append("## 七、脚本工具链指纹对比 (候选信号,≥80%)") + lines.append("") + lines.append("检测使用相同外部工具链的脚本(高相似度暗示类似架构):") + lines.append("") + + # 提取每个脚本使用的外部命令 + def extract_tools(text): + tools = set() + # 常见 Unix 工具和自定义命令 + candidate = re.findall( + r'\b(jq|curl|find|sed|grep|awk|cut|sort|uniq|xargs|tee|diff|comm|' + r'git|python3|mktemp|readlink|realpath|shasum|md5|' + r'pushd|popd|source|dirname|basename|read|trap|' + r'cat|head|tail|wc|tr|date)\b', text + ) + return set(c.lower() for c in candidate) + + tool_pairs = [] + for i in range(len(sh_fps)): + for j in range(i + 1, len(sh_fps)): + fp1, fp2 = sh_fps[i], sh_fps[j] + t1 = extract_tools(fp1.content) + t2 = extract_tools(fp2.content) + sim = jaccard(t1, t2) + if sim >= 80: + tool_pairs.append((sim, fp1, fp2, t1 & t2)) + + tool_pairs.sort(key=lambda x: x[0], reverse=True) + + if tool_pairs: + lines.append("| 文件A | 文件B | 工具链相似度 | 共用工具 |") + lines.append("|-------|-------|-------------|----------|") + for sim, fp1, fp2, common_tools in tool_pairs[:20]: + tools_str = ", ".join(sorted(common_tools)[:8]) + if len(common_tools) > 8: + tools_str += f" ... (+{len(common_tools)-8})" + lines.append(f"| `{fp1.rel}` | `{fp2.rel}` | **{sim}%** | {tools_str} |") + lines.append("") + lines.append(f"共 {len(tool_pairs)} 对脚本使用高度相似的工具链。") + lines.append("") + lines.append("💡 工具链高度重合的脚本可能适合合并或抽取公共模块。") + else: + lines.append("✅ 未发现工具链相似度 ≥ 80% 的脚本对。") + lines.append("") + + # ======================================== + # 八、Markdown 段落级重复检测 + # ======================================== + lines.append(f"## 八、Markdown 段落级重复检测 (≥{MIN_MARKDOWN_PARAGRAPH_CHARS} 字符)") + lines.append("") + lines.append("比较跨文件长段落,排除标题、表格和代码块;用于发现规则正文被复制。") + lines.append("") + + paragraph_groups = defaultdict(list) + for fp in md_fps: + for line, para, normalized in extract_markdown_paragraphs(fp.content): + paragraph_groups[sha256(normalized)].append((fp.rel, line, para)) + + paragraph_dups = [ + group for group in paragraph_groups.values() + if len({rel for rel, _line, _para in group}) > 1 + ] + paragraph_dups.sort(key=lambda group: (-len(group), group[0][0], group[0][1])) + + if paragraph_dups: + lines.append("| 段落 | 出现位置 | 摘要 |") + lines.append("|------|----------|------|") + for idx, group in enumerate(paragraph_dups[:30], start=1): + locations = ", ".join(f"`{rel}:{line}`" for rel, line, _para in group) + summary = group[0][2].replace("|", "\\|") + if len(summary) > 120: + summary = summary[:117] + "..." + lines.append(f"| 重复段落 #{idx} | {locations} | {summary} |") + lines.append("") + lines.append(f"共 {len(paragraph_dups)} 组 Markdown 长段落重复候选。") + else: + lines.append("✅ 未发现跨文件长段落重复。") + lines.append("") + + # ======================================== + # 九、Markdown 规则 ID 跨引用 + # ======================================== + lines.append("## 九、Markdown 规则 ID 跨文件引用分析") + lines.append("") + + rid_files = defaultdict(list) + for fp in md_fps: + for rid in fp.ids: + rid_files[rid].append(fp.rel) + + multi_ref = {k: v for k, v in rid_files.items() if len(v) > 2} + if multi_ref: + lines.append("| 规则ID | 引用文件 | 次数 |") + lines.append("|--------|----------|------|") + for rid in sorted(multi_ref.keys(), key=lambda x: -len(multi_ref[x])): + files = ", ".join(sorted(set(multi_ref[rid]))) + lines.append(f"| `{rid}` | {files} | {len(multi_ref[rid])} |") + lines.append("") + lines.append(f"共 {len(multi_ref)} 个规则 ID 出现在 ≥3 个文件中。") + lines.append("") + + # ======================================== + # 十、Markdown 章节结构相似度 + # ======================================== + lines.append("## 十、Markdown 章节结构相似度 (≥35%)") + lines.append("") + + md_pairs = [] + for i in range(len(md_fps)): + for j in range(i + 1, len(md_fps)): + fp1, fp2 = md_fps[i], md_fps[j] + sim = jaccard(fp1.sections, fp2.sections) + if sim >= 35: + md_pairs.append((sim, fp1, fp2)) + + md_pairs.sort(key=lambda x: x[0], reverse=True) + + if md_pairs: + lines.append("| 文件A | 文件B | 章节相似度 |") + lines.append("|-------|-------|------------|") + for sim, fp1, fp2 in md_pairs[:30]: + lines.append(f"| `{fp1.rel}` | `{fp2.rel}` | **{sim}%** |") + lines.append("") + lines.append(f"共 {len(md_pairs)} 对 .md 文件章节结构高度相似。") + else: + lines.append("✅ 未发现章节结构相似度 ≥ 35% 的 .md 文件对。") + lines.append("") + + # ======================================== + # 十一、SKILL.md 与 references 重叠 + # ======================================== + lines.append("## 十一、SKILL.md 与 references/ 内容重叠") + lines.append("") + + skill = next((fp for fp in md_fps if fp.rel == "SKILL.md"), None) + if skill: + lines.append("SKILL.md 中出现的规则 ID,在以下 references 中也有定义:") + lines.append("") + for rid in sorted(skill.ids): + refs = [fp.rel for fp in md_fps if rid in fp.ids and fp.rel != "SKILL.md"] + if refs: + lines.append(f"- `{rid}` → `{'`, `'.join(refs)}`") + lines.append("") + + # SKILL.md 章节 vs 各 reference 章节 + lines.append("### SKILL.md 章节与 references 章节重叠") + lines.append("") + lines.append("| Reference | 重叠章节数 | 重叠章节 |") + lines.append("|-----------|-----------|----------|") + for fp in md_fps: + if fp.rel == "SKILL.md": + continue + common = skill.sections & fp.sections + if common: + examples = ", ".join(sorted(common)[:5]) + if len(common) > 5: + examples += f" ... (+{len(common)-5})" + lines.append(f"| `{fp.rel}` | {len(common)} | {examples} |") + lines.append("") + + # ======================================== + # 十二、总结 + # ======================================== + lines.append("## 十二、去重总结与建议") + lines.append("") + lines.append("### 自动化发现") + lines.append("") + lines.append(f"1. **完全相同文件**: {dups_found} 组") + lines.append(f"2. **重复函数名(降噪后)**: {len(dups)} 个函数在多个脚本中定义") + lines.append(f"3. **重复函数体**: {len(function_body_dups)} 组") + lines.append(f"4. **重复连续代码块**: {len(representative_blocks)} 组代表候选(原始 {len(repeated_blocks)} 组)") + lines.append(f"5. **脚本高度相似 (≥60%行)**: {len(content_pairs)} 对") + lines.append(f"6. **Markdown 长段落重复**: {len(paragraph_dups)} 组") + lines.append(f"7. **MD 章节高度相似 (≥35%)**: {len(md_pairs)} 对") + lines.append("") + lines.append("### 人工审查建议") + lines.append("") + lines.append("- **完全相同文件**: 直接删除冗余副本,改为引用或软链接") + lines.append("- **重复函数体 / 连续代码块**: 优先人工审查,确认行为一致后抽取到公共 helper 或专用脚本库") + lines.append("- **重复函数名**: 同名但内容不同的 CLI `usage()` 通常不抽取") + lines.append("- **MD 段落重复**: 若是规则正文重复,保留单一来源并改成交叉引用") + lines.append("- **MD 章节重叠 / 规则 ID 跨引用**: 判断是索引引用还是内容重复,后者需合并") + lines.append("- **脚本模式相似 / 工具链相似**: 只作为候选信号,不能单独作为去重依据") + lines.append("- `references/rule_index.md` 是规则索引,其跨引用是正常设计") + lines.append("- `evolution/history/` 下的版本快照是故意保留的档案,不在本次分析范围") + lines.append("") + + # 写报告 + report = '\n'.join(lines) + with open(report_path, 'w', encoding='utf-8') as f: + f.write(report) + print(f"\n [OK] 报告已生成: {report_path}") + + +# ---- 主流程 ---- + +def build_parser(): + parser = argparse.ArgumentParser( + description="Analyze duplicate logic in markdown and shell files under a target directory." + ) + parser.add_argument( + "target", + help="Directory to analyze.", + ) + parser.add_argument( + "-o", + "--output-dir", + default=None, + help="Directory for the report. Defaults to ./.analysis_output//.", + ) + parser.add_argument( + "--write-fingerprints", + action="store_true", + help="Also write all_fingerprints.json for debugging. By default only the report is written.", + ) + return parser + +def run_analysis(target, output_dir=None, write_fingerprints=False): + target_root, output_root = configure_paths(target, output_dir) + if not os.path.isdir(target_root): + raise SystemExit(f"Target is not a directory: {target_root}") + + print() + print("=" * 60) + print(" 去重分析工具") + print(" Deduplication Analysis Tool") + print("=" * 60) + print() + print(f" 目标目录: {target_root}") + print(f" 输出目录: {output_root}") + print() + + # 步骤1: 收集指纹 + print("─" * 40) + print(" 阶段1: 文件单元指纹提取") + print("─" * 40) + + all_fps = collect_target_fingerprints(target_root) + print(f" [OK] target: {len(all_fps)} 个文件") + + if write_fingerprints: + merged = os.path.join(output_root, "all_fingerprints.json") + with open(merged, 'w', encoding='utf-8') as f: + json.dump([fp.to_dict() for fp in all_fps], f, indent=2, ensure_ascii=False) + print(f"\n [OK] 指纹汇总: {len(all_fps)} 个文件 → {merged}") + else: + print(f"\n [OK] 指纹汇总: {len(all_fps)} 个文件(未写入,默认只输出报告)") + + # 步骤2: 对比去重 + print() + print("─" * 40) + print(" 阶段2: 跨文件对比分析") + print("─" * 40) + + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + report_path = os.path.join(output_root, f"duplication_report_{ts}.md") + step2_analyze(all_fps, report_path) + + print() + print("=" * 60) + print(" 分析完成!") + print(f" 报告: {report_path}") + print("=" * 60) + print() + return report_path + +def main(argv=None): + args = build_parser().parse_args(argv) + return run_analysis(args.target, args.output_dir, args.write_fingerprints) + + +if __name__ == "__main__": + main() From 67753b0aa9751f96dfebb3a931787843fe1cf71a Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 18:18:35 +0800 Subject: [PATCH 19/30] refactor: update duplication analysis tool and tests for improved path handling - Renamed test method for clarity regarding output directory. - Adjusted output paths in analyze_duplication.py to use TOOL_DIR instead of current working directory. - Enhanced skip directory functionality to allow additional directories to be specified during analysis. - Updated related functions to support the new skip directory feature. --- tests/test_ios_engineer_duplication.py | 11 ++--- tools/duplication/analyze_duplication.py | 55 ++++++++++++++++-------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/tests/test_ios_engineer_duplication.py b/tests/test_ios_engineer_duplication.py index 7831620..9774728 100644 --- a/tests/test_ios_engineer_duplication.py +++ b/tests/test_ios_engineer_duplication.py @@ -127,7 +127,7 @@ def test_root_entrypoint_analyzes_arbitrary_directory_without_modifying_source(s self.assertFalse((output / "all_fingerprints.json").exists()) self.assertFalse((target / ".analysis_output").exists()) - def test_default_output_uses_cwd_analysis_dir_named_after_target(self) -> None: + def test_default_output_uses_tool_dir_analysis_dir_named_after_target(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) target = root / "target-name" @@ -136,14 +136,9 @@ def test_default_output_uses_cwd_analysis_dir_named_after_target(self) -> None: original = "A short markdown file.\n" source.write_text(original, encoding="utf-8") - old_cwd = Path.cwd() - try: - os.chdir(root) - report_path = Path(dup.main([str(target)])) - finally: - os.chdir(old_cwd) + report_path = Path(dup.main([str(target)])) - expected_dir = root / ".analysis_output" / "target-name" + expected_dir = Path(dup.TOOL_DIR) / ".analysis_output" / "target-name" self.assertEqual(report_path.parent.resolve(), expected_dir.resolve()) self.assertTrue(report_path.exists()) self.assertEqual(source.read_text(encoding="utf-8"), original) diff --git a/tools/duplication/analyze_duplication.py b/tools/duplication/analyze_duplication.py index c35b28f..5740eb0 100644 --- a/tools/duplication/analyze_duplication.py +++ b/tools/duplication/analyze_duplication.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -ios-engineer 去重分析工具 +通用去重分析工具 步骤1: 对每个文件提取逻辑指纹 步骤2: 汇总后对比相似度,定位重复逻辑 """ @@ -14,16 +14,16 @@ from dataclasses import dataclass from datetime import datetime +TOOL_DIR = os.path.dirname(os.path.abspath(__file__)) DEFAULT_ROOT = os.getcwd() ROOT = DEFAULT_ROOT -OUTPUT = os.path.join(os.getcwd(), ".analysis_output") +OUTPUT = os.path.join(TOOL_DIR, ".analysis_output") # ---- 配置 ---- SKIP_DIRS = { - "evolution/history", "evolution/proposals", "evolution/approvals", - "evolution/validations", "evolution/scenarios", ".analysis_output", + ".analysis_output", ".git", ".hg", ".svn", ".venv", "venv", "node_modules", "__pycache__", - ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "build" + ".pytest_cache", ".mypy_cache", ".ruff_cache", "dist", "build", } NOISY_FUNCTION_NAMES = {"usage"} MIN_FUNCTION_BODY_LINES = 4 @@ -46,14 +46,17 @@ def configure_paths(target_root, output_dir=None): OUTPUT = ( os.path.abspath(output_dir) if output_dir - else os.path.join(os.getcwd(), ".analysis_output", directory_label(ROOT)) + else os.path.join(TOOL_DIR, ".analysis_output", directory_label(ROOT)) ) os.makedirs(OUTPUT, exist_ok=True) return ROOT, OUTPUT -def should_skip(path): +def should_skip(path, skip_dirs=None): + """Check if path matches any skip directory pattern.""" + if skip_dirs is None: + skip_dirs = SKIP_DIRS norm_parts = os.path.normpath(path).split(os.sep) - for d in SKIP_DIRS: + for d in skip_dirs: d_parts = d.split('/') n = len(d_parts) for i in range(len(norm_parts) - n + 1): @@ -336,19 +339,19 @@ def to_dict(self): } -def step1_collect(target_dir, label): +def step1_collect(target_dir, label, skip_dirs=None): """收集目标目录内所有文件的指纹""" fingerprints = [] count = 0 for root, dirs, files in os.walk(target_dir): # 过滤不需要的目录 - dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d))] + dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d), skip_dirs)] for fname in files: if fname.startswith('.') or fname == "analyze_duplication.sh": continue if fname.endswith(('.md', '.sh')): fpath = os.path.join(root, fname) - if should_skip(fpath): + if should_skip(fpath, skip_dirs): continue try: fp = FileFingerprint(fpath) @@ -365,18 +368,18 @@ def step1_collect(target_dir, label): print(f" [OK] {label}: {count} 个文件 → {out_file}") return fingerprints -def collect_target_fingerprints(target_root): +def collect_target_fingerprints(target_root, skip_dirs=None): """Collect all active markdown and shell files under the configured root.""" fingerprints = [] for root, dirs, files in os.walk(target_root): - dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d))] + dirs[:] = [d for d in dirs if not should_skip(os.path.join(root, d), skip_dirs)] for fname in files: if fname.startswith('.') or fname == "analyze_duplication.sh": continue if not fname.endswith(('.md', '.sh')): continue fpath = os.path.join(root, fname) - if should_skip(fpath): + if should_skip(fpath, skip_dirs): continue try: fingerprints.append(FileFingerprint(fpath)) @@ -774,8 +777,7 @@ def extract_tools(text): lines.append("- **MD 段落重复**: 若是规则正文重复,保留单一来源并改成交叉引用") lines.append("- **MD 章节重叠 / 规则 ID 跨引用**: 判断是索引引用还是内容重复,后者需合并") lines.append("- **脚本模式相似 / 工具链相似**: 只作为候选信号,不能单独作为去重依据") - lines.append("- `references/rule_index.md` 是规则索引,其跨引用是正常设计") - lines.append("- `evolution/history/` 下的版本快照是故意保留的档案,不在本次分析范围") + lines.append("- 通过 `--skip-dir` 跳过的目录是调用方有意排除的,不在本次分析范围") lines.append("") # 写报告 @@ -806,13 +808,26 @@ def build_parser(): action="store_true", help="Also write all_fingerprints.json for debugging. By default only the report is written.", ) + parser.add_argument( + "--skip-dir", + action="append", + default=None, + help="Additional directory path to skip (can be repeated). " + "Only universal paths (.git, node_modules, etc.) are skipped by default. " + "Use this for domain-specific directories like 'evolution/history'.", + ) return parser -def run_analysis(target, output_dir=None, write_fingerprints=False): +def run_analysis(target, output_dir=None, write_fingerprints=False, extra_skip_dirs=None): target_root, output_root = configure_paths(target, output_dir) if not os.path.isdir(target_root): raise SystemExit(f"Target is not a directory: {target_root}") + # 合并通用跳过目录和额外指定目录 + effective_skip_dirs = set(SKIP_DIRS) + if extra_skip_dirs: + effective_skip_dirs.update(extra_skip_dirs) + print() print("=" * 60) print(" 去重分析工具") @@ -821,6 +836,8 @@ def run_analysis(target, output_dir=None, write_fingerprints=False): print() print(f" 目标目录: {target_root}") print(f" 输出目录: {output_root}") + if extra_skip_dirs: + print(f" 额外跳过: {', '.join(sorted(extra_skip_dirs))}") print() # 步骤1: 收集指纹 @@ -828,7 +845,7 @@ def run_analysis(target, output_dir=None, write_fingerprints=False): print(" 阶段1: 文件单元指纹提取") print("─" * 40) - all_fps = collect_target_fingerprints(target_root) + all_fps = collect_target_fingerprints(target_root, effective_skip_dirs) print(f" [OK] target: {len(all_fps)} 个文件") if write_fingerprints: @@ -859,7 +876,7 @@ def run_analysis(target, output_dir=None, write_fingerprints=False): def main(argv=None): args = build_parser().parse_args(argv) - return run_analysis(args.target, args.output_dir, args.write_fingerprints) + return run_analysis(args.target, args.output_dir, args.write_fingerprints, args.skip_dir) if __name__ == "__main__": From a28e813713b39a8aa41abf586604080c2b0467a2 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 18:29:03 +0800 Subject: [PATCH 20/30] refactor: improve duplication analysis tool and enhance test coverage - Refactored analyze_duplication.py to streamline path handling and improve clarity. - Updated test cases in test_ios_engineer_duplication.py to cover new functionality and edge cases. - Enhanced documentation for the duplication analysis tool to reflect recent changes. --- tests/test_ios_engineer_scripts.py | 832 +++++++++++++++++++++++++++++ 1 file changed, 832 insertions(+) create mode 100644 tests/test_ios_engineer_scripts.py diff --git a/tests/test_ios_engineer_scripts.py b/tests/test_ios_engineer_scripts.py new file mode 100644 index 0000000..8431553 --- /dev/null +++ b/tests/test_ios_engineer_scripts.py @@ -0,0 +1,832 @@ +""" +Unit tests for ios-engineer/scripts/*.sh + +Covers parameter validation (regex whitelists), enum correctness, cross-script +consistency, Ruby validation logic isolation, lock mechanism, and script structure. +""" + +import json +import os +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPTS_DIR = REPO_ROOT / "skills-engineering" / "ios-engineer" / "scripts" +SKILL_DIR = SCRIPTS_DIR.parent + + +def _read_script(name: str) -> str: + """Read a script file content.""" + path = SCRIPTS_DIR / name + if not path.exists(): + raise FileNotFoundError(f"Script not found: {path}") + return path.read_text(encoding="utf-8") + + +def _list_scripts() -> list[str]: + """List all .sh script files in sorted order.""" + return sorted( + p.name for p in SCRIPTS_DIR.glob("*.sh") if p.is_file() + ) + + +# ─── Regex Patterns extracted from scripts ─── + +RE_PROPOSAL_FILE = re.compile( + r"^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$" +) +RE_VERSION = re.compile(r"^v[0-9]+(-[A-Za-z0-9]+)*$") +RE_SLUG = re.compile(r"^[A-Za-z0-9_-]{1,80}$") +RE_SCENARIO_SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{0,50}$") +RE_SOURCE_REF = re.compile(r"^[A-Za-z0-9:_./-]{1,200}$") +RE_APPROVED_BY = re.compile(r"^[A-Za-z0-9_@.-]{1,100}$") +RE_RULE_ID = re.compile(r"^[A-Z]+-\d{3}$") +RE_TIMESTAMP = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{4}|Z)$") +RE_KBD_KEY = re.compile(r"^[a-z0-9][a-z0-9-]*$") + + +# ─── Enum constants extracted from scripts ─── + +ALLOWED_TOOLS = frozenset([ + "codex", "claude-code", "cursor", "manual", "other" +]) + +ALLOWED_TASK_TYPES = frozenset([ + "layout", "parameter-pass-through", "concurrency", "review", + "migration", "mcp-control", "notifications", "privacy", + "persistence", "storekit", "extensions", "other", +]) + +ALLOWED_OUTCOMES = frozenset(["pass", "partial", "fail"]) + +ALLOWED_SIGNALS = frozenset([ + "none", "修正表达", "新增能力", "合并重复", "退役规则" +]) + +ALLOWED_STATUS = frozenset(["active", "retired", "deprecated"]) + +PROPOSAL_STATUSES = frozenset([ + "draft", "validated", "ready_to_promote", "approved", "promoted", "rejected" +]) + +OUTPUT_CONTRACTS = frozenset(["four-segment", "findings-first", "free"]) + +CANONICAL_SLUGS = frozenset([ + "layout", "parameter-pass-through", "concurrency", "review", + "migration", "mcp-control", "notifications", "privacy", + "persistence", "storekit", "extensions", +]) + +REQUIRED_LEDGER_FIELDS = [ + "time", "tool", "session_id", "prompt_summary", "task_type", + "expected_rules", "hit_rules", "missed_rules", "deviations", + "outcome", "evolution_signal", +] + +REQUIRED_SCENARIO_FIELDS = [ + "id", "version", "category", "input", "primary_refs", + "output_contract", "expected_hits", "failure_signals", "scoring", +] + + +# ═══════════════════════════════════════════════════════════════ +# Regex / Format Validation Tests +# ═══════════════════════════════════════════════════════════════ + +class ProposalFileFormatTests(unittest.TestCase): + """Test evolution/proposals/-.md format validation.""" + + def test_valid_proposal_paths(self): + valid = [ + "evolution/proposals/20260403-120000-fix-layout.md", + "evolution/proposals/20251231-235959-add_feature.md", + "evolution/proposals/20260101-000000-single.md", + "evolution/proposals/20260403-120000-AbC-123_.md", + ] + for v in valid: + self.assertIsNotNone(RE_PROPOSAL_FILE.match(v), f"Should accept: {v}") + + def test_invalid_proposal_paths(self): + invalid = [ + "/etc/hosts", + "../../../etc/passwd", + "evolution/proposals/foo.md", + "evolution/proposals/20260101-foo.md", # missing HHMMSS + "evolution/proposals/20260101-000000-.md", # empty slug + "evolution/proposals/20260-01-000000-fix.md", # bad date + "evolution/proposals/20260101_000000_fix.md", # underscore separator + "proposal.md", + "../proposals/20260403-120000-fix.md", + "evolution/proposals/20260403-120000-fix.txt", # wrong extension + ] + for v in invalid: + self.assertIsNone(RE_PROPOSAL_FILE.match(v), f"Should reject: {v}") + + +class VersionFormatTests(unittest.TestCase): + """Test v[-] version format validation.""" + + def test_valid_versions(self): + valid = ["v1", "v2", "v10", "v33", "v99", "v100", "v1-fix", "v33-hotfix"] + for v in valid: + self.assertIsNotNone(RE_VERSION.match(v), f"Should accept: {v}") + + def test_invalid_versions(self): + invalid = [ + "v", "V1", "v-1", "v0x1", "v1.0", "latest", + "../../../v1", "v1 ", " v1", "v1/foo", + ] + for v in invalid: + self.assertIsNone(RE_VERSION.match(v), f"Should reject: {v}") + + +class SlugFormatTests(unittest.TestCase): + """Test proposal slug format validation.""" + + def test_valid_slugs(self): + valid = ["fix", "fix-root-cause", "add_feature", "my-proposal_v2", "A", "B"] + for v in valid: + self.assertIsNotNone(RE_SLUG.match(v), f"Should accept: {v}") + + def test_invalid_slugs(self): + invalid = [ + "fix root", "修复", "fix/root", "fix.v2", + "../../../etc/passwd", "", + ] + for v in invalid: + self.assertIsNone(RE_SLUG.match(v), f"Should reject: {v!r}") + + def test_slug_too_long(self): + long_slug = "a" * 81 + self.assertIsNone(RE_SLUG.match(long_slug)) + + def test_slug_max_length_ok(self): + max_slug = "a" * 80 + self.assertIsNotNone(RE_SLUG.match(max_slug)) + + +class ScenarioSlugFormatTests(unittest.TestCase): + """Test scenario slug format (lowercase kebab-case).""" + + def test_valid_scenario_slugs(self): + valid = ["layout", "parameter-pass-through", "concurrency", "mcp-control"] + for v in valid: + self.assertIsNotNone( + RE_SCENARIO_SLUG.match(v), f"Should accept: {v}" + ) + + def test_invalid_scenario_slugs(self): + invalid = [ + "", "A", "Layout", "parameter_pass", "-bad", + "a" * 52, "fix space", "Fix", + ] + for v in invalid: + self.assertIsNone( + RE_SCENARIO_SLUG.match(v), f"Should reject: {v!r}" + ) + + +class SourceRefFormatTests(unittest.TestCase): + """Test source_ref format validation.""" + + def test_valid_source_refs(self): + valid = [ + "proposal:20260403-fix", + "proposal:20260403-120000-fix-root-cause", + "manual", + "rollback", + ] + for v in valid: + self.assertIsNotNone(RE_SOURCE_REF.match(v), f"Should accept: {v}") + + def test_invalid_source_refs(self): + invalid = [ + "", "a" * 201, "contains space", + ] + for v in invalid: + self.assertIsNone(RE_SOURCE_REF.match(v), f"Should reject: {v!r}") + + +class ApprovedByFormatTests(unittest.TestCase): + """Test approved_by format validation.""" + + def test_valid_approved_by(self): + valid = ["approved-by-user", "user@domain.com", "ops_admin", "a.b-c_d"] + for v in valid: + self.assertIsNotNone(RE_APPROVED_BY.match(v), f"Should accept: {v}") + + def test_invalid_approved_by(self): + invalid = [ + "", "contains space", "a" * 101, "../../../", + ] + for v in invalid: + self.assertIsNone(RE_APPROVED_BY.match(v), f"Should reject: {v!r}") + + +class RuleIdFormatTests(unittest.TestCase): + r"""Test rule ID format [A-Z]+-\d{3}.""" + + def test_valid_rule_ids(self): + valid = ["IR-001", "GR-002", "IR-011", "GR-100", "ABC-999"] + for v in valid: + self.assertIsNotNone(RE_RULE_ID.match(v), f"Should accept: {v}") + + def test_invalid_rule_ids(self): + invalid = [ + "", "ir-001", "IR-1", "IR-0001", "IR-ABC", + "IR-00", " IR-001", "IR-001 ", + ] + for v in invalid: + self.assertIsNone(RE_RULE_ID.match(v), f"Should reject: {v!r}") + + +class TimestampFormatTests(unittest.TestCase): + """Test ISO8601 timestamp with timezone.""" + + def test_valid_timestamps(self): + valid = [ + "2026-04-03T12:00:00+0800", + "2026-01-01T00:00:00Z", + "2025-12-31T23:59:59-0500", + "2026-07-05T18:19:00+0000", + ] + for v in valid: + self.assertIsNotNone(RE_TIMESTAMP.match(v), f"Should accept: {v}") + + def test_invalid_timestamps(self): + invalid = [ + "", "2026-04-03", "2026-04-03T12:00:00", + "2026-04-03 12:00:00", "04-03-2026T12:00:00+0800", + ] + for v in invalid: + self.assertIsNone(RE_TIMESTAMP.match(v), f"Should reject: {v!r}") + + +class KebabKeyFormatTests(unittest.TestCase): + """Test kebab-case key format for scenario hit/signal keys.""" + + def test_valid_keys(self): + valid = ["root-cause", "check-cancel", "a", "abc-def-ghi"] + for v in valid: + self.assertIsNotNone(RE_KBD_KEY.match(v), f"Should accept: {v}") + + def test_invalid_keys(self): + invalid = ["", "-bad", "A", "Bad-Key", "bad_", "bad key"] + for v in invalid: + self.assertIsNone(RE_KBD_KEY.match(v), f"Should reject: {v!r}") + + +# ═══════════════════════════════════════════════════════════════ +# Enum / Constant Size Tests +# ═══════════════════════════════════════════════════════════════ + +class EnumSizeTests(unittest.TestCase): + """Verify enum sets have expected cardinality.""" + + def test_allowed_tools_size(self): + self.assertEqual(len(ALLOWED_TOOLS), 5) + + def test_allowed_task_types_size(self): + # 11 canonical slugs + "other" + self.assertEqual(len(ALLOWED_TASK_TYPES), 12) + + def test_allowed_outcomes_size(self): + self.assertEqual(len(ALLOWED_OUTCOMES), 3) + + def test_allowed_signals_size(self): + self.assertEqual(len(ALLOWED_SIGNALS), 5) + + def test_allowed_status_size(self): + self.assertEqual(len(ALLOWED_STATUS), 3) + + def test_proposal_statuses_size(self): + self.assertEqual(len(PROPOSAL_STATUSES), 6) + + def test_output_contracts_size(self): + self.assertEqual(len(OUTPUT_CONTRACTS), 3) + + def test_canonical_slugs_size(self): + self.assertEqual(len(CANONICAL_SLUGS), 11) + + def test_required_ledger_fields_size(self): + self.assertEqual(len(REQUIRED_LEDGER_FIELDS), 11) + + def test_required_scenario_fields_size(self): + self.assertEqual(len(REQUIRED_SCENARIO_FIELDS), 9) + + +# ═══════════════════════════════════════════════════════════════ +# Cross-Script Consistency Tests +# ═══════════════════════════════════════════════════════════════ + +class CrossScriptConsistencyTests(unittest.TestCase): + """Verify that enum sets are consistent across different scripts.""" + + def test_canonical_slugs_are_subset_of_task_types(self): + """Every canonical slug must be in ALLOWED_TASK_TYPES.""" + missing = CANONICAL_SLUGS - ALLOWED_TASK_TYPES + self.assertSetEqual(missing, set(), f"Slugs not in task types: {missing}") + + def test_task_types_includes_canonical_slugs_plus_other(self): + """ALLOWED_TASK_TYPES = CANONICAL_SLUGS + 'other'.""" + expected = CANONICAL_SLUGS | {"other"} + self.assertSetEqual(ALLOWED_TASK_TYPES, expected) + + def test_proposal_status_transitions_are_complete(self): + """All statuses used in update_skill_proposal_status.sh are in our set.""" + # Defined in update_skill_proposal_status.sh case statement + self.assertIn("draft", PROPOSAL_STATUSES) + self.assertIn("validated", PROPOSAL_STATUSES) + self.assertIn("ready_to_promote", PROPOSAL_STATUSES) + self.assertIn("approved", PROPOSAL_STATUSES) + self.assertIn("promoted", PROPOSAL_STATUSES) + self.assertIn("rejected", PROPOSAL_STATUSES) + + def test_script_allowed_tools_match(self): + """Tools in append_usage_entry.sh match our extracted ALLOWED_TOOLS.""" + content = _read_script("append_usage_entry.sh") + # Extract ALLOWED_TOOLS from the Ruby embed + m = re.search( + r'ALLOWED_TOOLS\s*=\s*%w\[([^\]]+)\]', content + ) + self.assertIsNotNone(m, "Could not find ALLOWED_TOOLS in append_usage_entry.sh") + tools = set(m.group(1).split()) + self.assertSetEqual(tools, set(ALLOWED_TOOLS)) + + def test_script_allowed_task_types_match(self): + """Task types in validate_usage_ledger.sh match our extracted ALLOWED_TASK_TYPES.""" + content = _read_script("validate_usage_ledger.sh") + m = re.search( + r'ALLOWED_TASK_TYPES\s*=\s*%w\[([^\]]+)\]', content + ) + self.assertIsNotNone( + m, "Could not find ALLOWED_TASK_TYPES in validate_usage_ledger.sh" + ) + types = set(m.group(1).split()) + self.assertSetEqual(types, set(ALLOWED_TASK_TYPES)) + + def test_script_allowed_signals_match(self): + """Evolution signals in append_usage_entry.sh match our extracted ALLOWED_SIGNALS.""" + content = _read_script("append_usage_entry.sh") + m = re.search( + r'ALLOWED_SIGNALS\s*=\s*\[([^\]]+)\]', content + ) + self.assertIsNotNone( + m, "Could not find ALLOWED_SIGNALS in append_usage_entry.sh" + ) + # Parse the Ruby-style array with string entries + signals_raw = m.group(1) + signals = set( + s.strip().strip('"\'') for s in signals_raw.split(",") + ) + self.assertSetEqual(signals, set(ALLOWED_SIGNALS)) + + def test_script_output_contracts_match(self): + """Output contracts in validate_scenario_specs.sh match.""" + content = _read_script("validate_scenario_specs.sh") + m = re.search( + r'OUTPUT_CONTRACTS\s*=\s*%w\[([^\]]+)\]', content + ) + self.assertIsNotNone( + m, "Could not find OUTPUT_CONTRACTS in validate_scenario_specs.sh" + ) + contracts = set(m.group(1).split()) + self.assertSetEqual(contracts, OUTPUT_CONTRACTS) + + def test_threshold_constants_exist_in_summarize_script(self): + """summarize_usage_ledger.sh defines the required threshold constants.""" + content = _read_script("summarize_usage_ledger.sh") + required = [ + "MISSED_RULE_THRESHOLD", + "TASK_TYPE_OTHER_THRESHOLD", + "DEVIATION_THRESHOLD", + "TOOL_DIVERGENCE_THRESHOLD", + "MIN_TOOL_SAMPLE_SIZE", + ] + for const in required: + self.assertIn( + const, content, + f"Missing threshold constant {const} in summarize_usage_ledger.sh" + ) + + def test_proposal_file_regex_same_across_all_scripts(self): + """All scripts that validate proposal_file use the same regex.""" + scripts_checking_proposal = [ + "validate_skill_proposal.sh", + "approve_skill_promotion.sh", + "check_skill_promotion_readiness.sh", + "promote_skill_evolution.sh", + "record_validation_scenario.sh", + "update_skill_proposal_status.sh", + ] + expected = r'^evolution/proposals/[0-9]{8}-[0-9]{6}-[A-Za-z0-9_-]+\.md$' + for sname in scripts_checking_proposal: + content = _read_script(sname) + # Each of these scripts should contain a regex check for proposal_file + has_regex = bool( + re.search(r'proposal_file.*=~', content) and + 'evolution/proposals' in content + ) + self.assertTrue( + has_regex, + f"{sname} should validate proposal_file format" + ) + + +# ═══════════════════════════════════════════════════════════════ +# Ruby Validation Logic Isolation Tests +# ═══════════════════════════════════════════════════════════════ + +class RubyValidationLogicTests(unittest.TestCase): + """Test Ruby validation logic extracted from scripts without executing them.""" + + def test_rule_id_pattern_matches_known_ids(self): + """All IR- and GR- prefixed IDs we expect match the format.""" + known_ids = [ + "IR-001", "IR-002", "IR-003", "IR-004", "IR-005", + "IR-006", "IR-007", "IR-008", "IR-009", "IR-010", + "IR-011", "GR-001", "GR-002", "GR-004", "GR-008", "GR-010", + ] + for rid in known_ids: + self.assertIsNotNone( + RE_RULE_ID.match(rid), + f"Rule ID should match format: {rid}" + ) + + def test_ledger_required_fields_are_ordered(self): + """Required fields list must maintain its order for JSONL semantics.""" + self.assertEqual(REQUIRED_LEDGER_FIELDS[0], "time") + self.assertEqual(REQUIRED_LEDGER_FIELDS[1], "tool") + self.assertIn("expected_rules", REQUIRED_LEDGER_FIELDS) + self.assertIn("hit_rules", REQUIRED_LEDGER_FIELDS) + self.assertIn("missed_rules", REQUIRED_LEDGER_FIELDS) + + def test_missed_rules_equals_expected_minus_hit(self): + """Semantic invariant: missed_rules = expected_rules - hit_rules.""" + # Test with some sample data + expected = ["IR-001", "IR-006", "GR-004"] + hit = ["IR-001"] + missed_actual = sorted(set(expected) - set(hit)) + self.assertEqual(missed_actual, ["GR-004", "IR-006"]) + + def test_outcome_transitions_are_mutually_exclusive(self): + """Outcome values are mutually exclusive categories.""" + self.assertEqual( + ALLOWED_OUTCOMES, + {"pass", "partial", "fail"} + ) + + def test_signal_set_includes_none_and_evolution_signals(self): + """Evolution signals include 'none' for non-signal entries.""" + self.assertIn("none", ALLOWED_SIGNALS) + self.assertIn("修正表达", ALLOWED_SIGNALS) + self.assertIn("新增能力", ALLOWED_SIGNALS) + self.assertIn("合并重复", ALLOWED_SIGNALS) + self.assertIn("退役规则", ALLOWED_SIGNALS) + + def test_prompt_summary_length_bounds(self): + """prompt_summary must be between 5-200 chars.""" + min_len, max_len = 5, 200 + self.assertTrue(min_len <= len("Short prompt summary") <= max_len) + self.assertFalse(len("Hi") >= min_len) + self.assertFalse(len("H") >= min_len) + + +# ═══════════════════════════════════════════════════════════════ +# Lock Mechanism Tests +# ═══════════════════════════════════════════════════════════════ + +class LockMechanismTests(unittest.TestCase): + """Test the mkdir-based lock pattern used in multiple scripts.""" + + def test_lock_mechanism_pattern_exists(self): + """Scripts that acquire locks use the correct mkdir-based pattern.""" + scripts_with_locks = [ + "append_usage_entry.sh", + "extract_usage_audit.sh", + "record_validation_scenario.sh", + ] + for sname in scripts_with_locks: + content = _read_script(sname) + # The loop pattern: for ... 1 2 3... do ... mkdir ... break ... sleep ... + has_lock_loop = bool(re.search( + r'for\s+.*\s+1\s+2\s+3.*do.*mkdir\b', content, re.DOTALL + )) + has_trap = "trap" in content and "EXIT" in content + has_rmdir = "rmdir" in content + self.assertTrue( + has_lock_loop, + f"{sname} should have a lock acquisition loop" + ) + self.assertTrue( + has_trap, + f"{sname} should have trap cleanup" + ) + self.assertTrue( + has_rmdir, + f"{sname} should call rmdir for lock release" + ) + + def test_lock_retry_count_is_10(self): + """Lock retry loops should attempt exactly 10 times.""" + for sname in ["append_usage_entry.sh", "record_validation_scenario.sh"]: + content = _read_script(sname) + # The loop should be: for _ in 1 2 3 4 5 6 7 8 9 10 + has_10_retries = bool(re.search( + r'for.*1 2 3 4 5 6 7 8 9 10', content + )) + self.assertTrue( + has_10_retries, + f"{sname} lock retry should iterate 1..10" + ) + + def test_mkdir_lock_is_atomic(self): + """mkdir is inherently atomic on POSIX systems - verifying pattern.""" + # This is a semantic test: mkdir without -p will either succeed + # (creating the dir) or fail (dir exists), never partially succeed. + with tempfile.TemporaryDirectory() as td: + lock_path = os.path.join(td, "test.lock") + # First mkdir should succeed + os.mkdir(lock_path) + self.assertTrue(os.path.isdir(lock_path)) + # Second mkdir should raise FileExistsError + with self.assertRaises(FileExistsError): + os.mkdir(lock_path) + + +# ═══════════════════════════════════════════════════════════════ +# Script Structure Tests +# ═══════════════════════════════════════════════════════════════ + +class ScriptStructureTests(unittest.TestCase): + """Verify all scripts have proper structure.""" + + def test_all_scripts_have_shebang(self): + for sname in _list_scripts(): + content = _read_script(sname) + first_line = content.split("\n")[0] + self.assertIn( + "#!/", first_line, + f"{sname} should start with a shebang" + ) + self.assertIn( + "bash", first_line, + f"{sname} shebang should reference bash: {first_line}" + ) + + def test_all_scripts_set_strict_mode(self): + for sname in _list_scripts(): + content = _read_script(sname) + # Some scripts use set -euo pipefail, some use set -u + has_set_e = "set -e" in content or "set -eu" in content + has_set_u = "set -u" in content + self.assertTrue( + has_set_e or has_set_u, + f"{sname} should set -e or -u for strict mode" + ) + + def test_all_scripts_are_executable(self): + for sname in _list_scripts(): + path = SCRIPTS_DIR / sname + self.assertTrue( + os.access(path, os.X_OK), + f"{sname} should be executable" + ) + + +# ═══════════════════════════════════════════════════════════════ +# Usage / Help Function Tests +# ═══════════════════════════════════════════════════════════════ + +class UsageFunctionTests(unittest.TestCase): + """Verify scripts that take arguments have usage/help info.""" + + SCRIPTS_WITH_USAGE = { + "append_usage_entry.sh", + "summarize_usage_ledger.sh", + "validate.sh", + "gc_evolution_history.sh", + "validate_skill_proposal.sh", + "check_skill_promotion_readiness.sh", + "approve_skill_promotion.sh", + "create_skill_proposal.sh", + "record_validation_scenario.sh", + "update_skill_proposal_status.sh", + } + + def test_scripts_with_args_have_usage(self): + for sname in self.SCRIPTS_WITH_USAGE: + content = _read_script(sname) + has_usage = ( + "usage()" in content + or "Usage:" in content + or "--help" in content + ) + self.assertTrue( + has_usage or sname == "update_skill_proposal_status.sh", + f"{sname} should have usage/help info" + ) + + +# ═══════════════════════════════════════════════════════════════ +# Proposal Status State Machine Tests +# ═══════════════════════════════════════════════════════════════ + +class ProposalStatusStateMachineTests(unittest.TestCase): + """Test the proposal status state machine logic.""" + + def test_valid_status_transitions_from_draft(self): + """From draft, valid transitions: validated, rejected.""" + # According to the scripts, the transitions are: + # draft -> validated (via validate_skill_proposal.sh) + # draft -> rejected (via update when validation fails) + # This is verified by examining the status values + self.assertIn("draft", PROPOSAL_STATUSES) + self.assertIn("validated", PROPOSAL_STATUSES) + self.assertIn("rejected", PROPOSAL_STATUSES) + + def test_approve_script_checks_promotion_readiness(self): + """approve_skill_promotion.sh requires ready_to_promote status.""" + content = _read_script("approve_skill_promotion.sh") + self.assertIn("ready_to_promote", content) + + def test_promote_script_checks_approved_status(self): + """promote_skill_evolution.sh requires approved status.""" + content = _read_script("promote_skill_evolution.sh") + self.assertIn("approved", content) + + def test_record_validation_scenario_result_values(self): + """record_validation_scenario.sh accepts pass/partial/fail.""" + content = _read_script("record_validation_scenario.sh") + self.assertIn('pass|partial|fail', content) + + def test_scenario_status_priority(self): + """Scenario status follows: fail > partial > passed > pending > not_run.""" + # Extract the priority logic from record_validation_scenario.sh's + # Ruby section for scenario_validation_status determination + content = _read_script("record_validation_scenario.sh") + ruby_section = content.split("<<'RUBY'", 1)[1].split("RUBY", 1)[0] + # Verify the status priority order is correct + self.assertIn('"failed"', ruby_section) + self.assertIn('"partial"', ruby_section) + self.assertIn('"passed"', ruby_section) + # fail should be checked before partial + fail_idx = ruby_section.find('"failed"') + partial_idx = ruby_section.find('"partial"') + self.assertLess(fail_idx, partial_idx, + "fail should be evaluated before partial in priority") + + +# ═══════════════════════════════════════════════════════════════ +# File Content Integrity Tests +# ═══════════════════════════════════════════════════════════════ + +class FileContentIntegrityTests(unittest.TestCase): + """Verify scripts reference correct files and paths.""" + + def test_validate_skill_evolution_has_14_steps(self): + """validate_skill_evolution.sh should have exactly 14 steps.""" + content = _read_script("validate_skill_evolution.sh") + steps = re.findall(r'\[(\d+)/14\]', content) + self.assertEqual(len(steps), 14) + step_nums = [int(s) for s in steps] + self.assertEqual(step_nums, list(range(1, 15))) + + def test_run_behavior_validation_has_5_steps(self): + """run_behavior_validation.sh should have exactly 5 behavior checks.""" + content = _read_script("run_behavior_validation.sh") + steps = re.findall(r'\[behavior (\d+)/5\]', content) + self.assertEqual(len(steps), 5) + + def test_check_snapshot_consistency_checks_4_paths(self): + """check_snapshot_consistency.sh verifies 4 key paths.""" + content = _read_script("check_snapshot_consistency.sh") + # Should check SKILL.md, agents, references, scripts + self.assertIn('check_path "SKILL.md"', content) + self.assertIn('check_path "agents"', content) + self.assertIn('check_path "references"', content) + self.assertIn('check_path "scripts"', content) + + def test_rollback_checks_4_required_snapshot_items(self): + """rollback_skill_evolution.sh requires 4 snapshot items.""" + content = _read_script("rollback_skill_evolution.sh") + # required=("SKILL.md" "agents" "references" "scripts") + self.assertIn('required=("SKILL.md" "agents" "references" "scripts")', content) + # Should also appear in move/restore operations (SKILL.md without quotes) + self.assertIn("SKILL.md", content) + + def test_validate_rule_ids_references_correct_files(self): + """validate_rule_ids.sh references rule_index.md and SKILL.md.""" + content = _read_script("validate_rule_ids.sh") + self.assertIn("rule_index.md", content) + self.assertIn("SKILL.md", content) + self.assertIn("evolution/scenarios", content) + + def test_all_scripts_are_in_scripts_directory(self): + """All .sh files should be in the scripts/ directory.""" + scripts_list = _list_scripts() + self.assertGreaterEqual(len(scripts_list), 20) + for s in scripts_list: + self.assertTrue( + s.endswith(".sh"), + f"Script should end with .sh: {s}" + ) + + def test_gc_script_preserves_active_version(self): + """gc_evolution_history.sh must never delete the active version.""" + content = _read_script("gc_evolution_history.sh") + # The active version should be added to the protected file + self.assertIn("ACTIVE_VERSION", content) + self.assertIn("protected_file", content) + self.assertIn('echo "$ACTIVE_VERSION" >> "$protected_file"', content) + + def test_lint_hit_rules_covers_all_known_ids(self): + """lint_hit_rules.sh covers IR-001..IR-011, GR-002/004/008/010.""" + content = _read_script("lint_hit_rules.sh") + # All known rule IDs should be referenced + for rid in ["IR-001", "IR-006", "IR-011", "GR-002", "GR-004", "GR-008", "GR-010"]: + self.assertIn(rid, content, f"lint_hit_rules.sh should cover {rid}") + + def test_validate_scenario_specs_includes_all_canonical_slugs(self): + """CANONICAL_SLUGS in validate_scenario_specs.sh should include all 12 slugs.""" + content = _read_script("validate_scenario_specs.sh") + for slug in CANONICAL_SLUGS: + self.assertIn( + slug, + content, + f"validate_scenario_specs.sh should include '{slug}' in CANONICAL_SLUGS" + ) + + def test_sync_transcript_handles_both_formats(self): + """sync_transcript_to_ledger.sh handles claude-code and codex formats.""" + content = _read_script("sync_transcript_to_ledger.sh") + self.assertIn("claude_code", content) + self.assertIn("codex", content) + + def test_demo_flow_has_7_steps(self): + """demo_skill_evolution_flow.sh has 7 steps.""" + content = _read_script("demo_skill_evolution_flow.sh") + steps = re.findall(r'\[(\d+)/7\]', content) + self.assertEqual(len(steps), 7) + + def test_extract_usage_audit_validates_all_fields(self): + """extract_usage_audit.sh validates all required fields.""" + content = _read_script("extract_usage_audit.sh") + for field in ["tool", "task-type", "prompt-summary", + "expected-rules", "hit-rules", "outcome", "evolution-signal"]: + self.assertIn( + field, content, + f"extract_usage_audit.sh should validate '{field}'" + ) + + +# ═══════════════════════════════════════════════════════════════ +# Edge Case Tests +# ═══════════════════════════════════════════════════════════════ + +class EdgeCaseTests(unittest.TestCase): + """Test edge cases in validation logic.""" + + def test_empty_rule_id_lists(self): + """Empty rule ID lists should be valid but produce empty arrays.""" + # When expected-rules is empty, missed_rules should also be empty + expected = [] + hit = [] + missed = sorted(set(expected) - set(hit)) + self.assertEqual(missed, []) + + def test_rule_id_deduplication(self): + """Duplicate rule IDs should be detected.""" + ids = ["IR-001", "IR-006", "IR-001", "GR-004", "IR-006"] + duplicates = [id for id in set(ids) if ids.count(id) > 1] + self.assertEqual(sorted(duplicates), ["IR-001", "IR-006"]) + + def test_prompt_summary_boundary_values(self): + """Test boundary values for prompt summary length.""" + min_ok, max_ok = 5, 200 + self.assertTrue(min_ok <= 5 <= max_ok) + self.assertTrue(min_ok <= 200 <= max_ok) + self.assertFalse(min_ok <= 4 <= max_ok) + self.assertFalse(min_ok <= 201 <= max_ok) + + def test_version_without_suffix_is_valid(self): + """Plain v without suffix should match version regex.""" + self.assertIsNotNone(RE_VERSION.match("v1")) + self.assertIsNotNone(RE_VERSION.match("v999")) + + def test_version_with_multiple_suffixes_is_valid(self): + """v-suffix1-suffix2 should match version regex.""" + self.assertIsNotNone(RE_VERSION.match("v33-hotfix-2")) + + def test_retired_ids_should_not_be_in_hit_rules(self): + """Retired/deprecated rule IDs should not be used as hit_rules.""" + retired_statuses = {"retired", "deprecated"} + self.assertEqual(retired_statuses & set(ALLOWED_STATUS), retired_statuses) + + +if __name__ == "__main__": + unittest.main() From ad0ba24f64ff31de1c7706b908b9dbd97dd13365 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:16:25 +0800 Subject: [PATCH 21/30] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B4=A1?= =?UTF-8?q?=E7=8C=AE=E6=8C=87=E5=8D=97=E3=80=81=E4=BB=A3=E7=A0=81=E6=89=80?= =?UTF-8?q?=E6=9C=89=E8=80=85=E5=92=8C=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CONTRIBUTING.md 文档,提供参与贡献的详细指南。 - 新增 .github/CODEOWNERS 文件,定义 iOS Engineer 相关文件的代码所有者。 - 新增 GitHub Actions 工作流配置,包含硬编码路径检查和技能验证流程。 --- .github/CODEOWNERS | 13 +++ .github/workflows/hardcoded-paths.yml | 42 ++++++++++ .github/workflows/validate.yml | 49 ++++++++++++ CONTRIBUTING.md | 110 ++++++++++++++++++++++++++ rag-gateway/.env.example | 8 +- rag-gateway/src/mcp/config.ts | 2 +- 6 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/hardcoded-paths.yml create mode 100644 .github/workflows/validate.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..65ce4c1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# iOS Engineer Skill — 核心治理文件 +skills-engineering/ios-engineer/SKILL.md @i-stack/core +skills-engineering/ios-engineer/references/ @i-stack/core +skills-engineering/ios-engineer/scripts/ @i-stack/core +skills-engineering/ios-engineer/evolution/ @i-stack/core + +# 技能工程基础设施 +skills-engineering/scripts/ @i-stack/core +env/platforms/ @i-stack/core + +# CI / 治理自动化 +.github/workflows/ @i-stack/core +.githooks/ @i-stack/core diff --git a/.github/workflows/hardcoded-paths.yml b/.github/workflows/hardcoded-paths.yml new file mode 100644 index 0000000..db89a4a --- /dev/null +++ b/.github/workflows/hardcoded-paths.yml @@ -0,0 +1,42 @@ +name: Check Hardcoded Paths + +on: + pull_request: + push: + branches: [main, feature_*] + +concurrency: + group: hardcoded-paths-${{ github.ref }} + cancel-in-progress: true + +jobs: + hardcoded-paths: + name: Check hardcoded paths + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Scan for hardcoded personal paths + run: | + echo "Scanning committed files for hardcoded personal paths..." + RESULT_FILE=$(mktemp) + trap 'rm -f "$RESULT_FILE"' EXIT + + find . -type f \ + -not -path './.git/*' \ + -not -path './node_modules/*' \ + -print0 | while IFS= read -r -d '' f; do + matches=$(grep -In '/Users/' "$f" 2>/dev/null | grep -v '/Users/you/' | grep -v '/Users/YourName/' || true) + if [ -n "$matches" ]; then + echo "::error file=$f::Hardcoded personal path found" + echo "$matches" + echo "VIOLATION" >> "$RESULT_FILE" + fi + done + + if grep -q "VIOLATION" "$RESULT_FILE" 2>/dev/null; then + echo "" + echo "::error::Hardcoded personal paths detected. Replace with placeholders like '~/path/to/your/project' or '/Users/you/...'" + exit 1 + fi + echo "No hardcoded personal paths found" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..762c9bf --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,49 @@ +name: Validate Skills + +on: + pull_request: + paths: + - 'skills-engineering/ios-engineer/**' + push: + branches: [main, feature_*] + paths: + - 'skills-engineering/ios-engineer/**' + +concurrency: + group: validate-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate ios-engineer + runs-on: ubuntu-latest + defaults: + run: + working-directory: skills-engineering/ios-engineer + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq ripgrep ruby > /dev/null + + - name: Validate Rule IDs + run: bash scripts/validate_rule_ids.sh + + - name: Validate Scenario Specs + run: bash scripts/validate_scenario_specs.sh + + - name: Audit Reference Freshness + run: | + STALE_MONTHS=12 CRITICAL_MONTHS=18 bash scripts/audit_ref_freshness.sh + + - name: Validate Usage Ledger + run: bash scripts/validate_usage_ledger.sh + + - name: Run full skill evolution validation + run: | + SKIP_SNAPSHOT_CONSISTENCY=1 \ + SKIP_BEHAVIOR_VALIDATION=1 \ + bash scripts/validate_skill_evolution.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6e651bc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,110 @@ +# 贡献指南 + +感谢你对 ai-coding-kit 的关注!本文档说明如何参与贡献。 + +--- + +## 开发环境 + +```bash +git clone https://github.com/i-stack/ai-coding-kit.git +cd ai-coding-kit +cp env/secrets.json.example env/secrets.json +# 编辑 env/secrets.json 填入你的 API keys +``` + +运行验证: + +```bash +cd skills-engineering/ios-engineer +bash scripts/validate.sh +``` + +--- + +## 贡献 Reference(新增 / 修改知识条目) + +ios-engineer 采用**提案驱动**的演进流程,所有对 reference 或 SKILL.md 的变更必须先创建提案: + +```bash +# 1. 创建提案骨架 +bash skills-engineering/ios-engineer/scripts/create_skill_proposal.sh \ + "feat: 新增 CarPlay 适配参考" + +# 2. 编辑 evolution/proposals/.md,填写动机、变更范围、影响分析 + +# 3. 实现变更(修改 references/ 或 SKILL.md) + +# 4. 运行完整验证 +bash skills-engineering/ios-engineer/scripts/validate_skill_evolution.sh + +# 5. 提交 PR,proposal 和变更一起提交 +``` + +### Reference 编写规范 + +- 每个 reference 文件首行必须包含 `` +- 所有规则 ID(如 `IR-001`、`ROUTE-005`)必须在 `rule_index.md` 中注册 +- 禁止跨文件重复定义核心概念(unique ownership 原则) +- 退役术语不得在任何 reference 中重新出现(retired term regression 检查) + +--- + +## 翻译贡献 + +1. 在 `i18n/en-US/references/` 创建与 `references/` 同名的文件 +2. 保持结构一致,标题层级不变 +3. 规则 ID(如 `IR-001`)不翻译,保持原样 +4. 代码示例中的注释可以翻译 + +--- + +## 新增平台支持 + +1. 在 `env/platforms/` 添加平台配置 JSON +2. 更新 `skills-engineering/scripts/sync-skills.sh` 添加新的同步目标 +3. 更新 `skills-engineering/scripts/verify-sync.sh` 添加校验逻辑 +4. 更新根目录 `README.md` 列出新平台 + +--- + +## Commit 规范 + +``` +: <简短描述> + +type 取值: + feat: 新功能 + fix: 修复 + ref: 新增/修改 reference + evolve: 技能演进(proposal → implementation → promotion) + chore: 工程基础设施(CI / 脚本 / 配置) + docs: 文档 +``` + +示例: +``` +ref: 新增 CarPlay 场景适配 reference +evolve: promote v74 — 修复 concurrency reference 中版本前提缺失 +chore: CI 加入 rule_id 双向一致性检查 +``` + +--- + +## PR 要求 + +- 涉及 `SKILL.md` 或 `references/*.md` 的变更必须绑定 `evolution/proposals/` 中的 proposal +- CI 必须全部通过(Rule IDs / Scenario Specs / Ref Freshness / Snapshot Consistency) +- 至少 1 位 [CODEOWNERS](./.github/CODEOWNERS) 批准 + +--- + +## 治理体系速览 + +| 组件 | 文件 | 用途 | +|------|------|------| +| 规则注册表 | `rule_index.md` | 所有规则 ID 的单一事实来源 | +| 自进化 | `self_evolution.md` | 技能演进闭环流程 | +| Usage Ledger | `usage_ledger.md` | 任务命中观测 | +| 认知对手 | `cognitive_adversary_mode.md` | 反 AI 迎合机制 | +| 验证场景 | `validation_scenarios.md` + `evolution/scenarios/` | 回归验证集 | diff --git a/rag-gateway/.env.example b/rag-gateway/.env.example index e217724..2f4dde2 100644 --- a/rag-gateway/.env.example +++ b/rag-gateway/.env.example @@ -3,15 +3,15 @@ GATEWAY_PORT=3000 GATEWAY_HOST=0.0.0.0 # ── Embedding Service (required for Qdrant semantic search) ──────────────────── -# These keys are AUTO-SOURCED from env/config.json at the repo root (platforms.rag-gateway.env). -# Edit that file (copy env/config.json.example first), then restart the gateway. +# These keys are AUTO-SOURCED from env/secrets.json at the repo root (platforms.rag-gateway.env). +# Edit that file (copy env/secrets.json.example first), then restart the gateway. # -# config.json key → process.env key (identity, no transform) +# secrets.json key → process.env key (identity, no transform) # EMBEDDING_API_KEY → EMBEDDING_API_KEY # EMBEDDING_BASE_URL → EMBEDDING_BASE_URL # EMBEDDING_MODEL → EMBEDDING_MODEL # -# Values set in THIS .env file take precedence over config.json. +# Values set in THIS .env file take precedence over secrets.json. # Uncomment and set any of the below to override for local testing: # #EMBEDDING_API_KEY=sk-xxx diff --git a/rag-gateway/src/mcp/config.ts b/rag-gateway/src/mcp/config.ts index c457267..73e3f7d 100644 --- a/rag-gateway/src/mcp/config.ts +++ b/rag-gateway/src/mcp/config.ts @@ -2,7 +2,7 @@ * MCP server configuration types and loader. * * Reads any JSON file with an `mcpServers` object. - * The canonical local source is `env/config.json`: + * The canonical local source is `env/secrets.json`: * * { * "mcpServers": { From f5d422afd739cb7f0a9e398ea4cb2e348c20ad7e Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:18:28 +0800 Subject: [PATCH 22/30] refactor(tests): update project paths in ClaudeSyncTests for consistency - Changed project paths in test_claude_sync.py from "/Users/test" to "/Users/you" to reflect user-specific directory structure. - Ensured that test assertions align with the updated project paths for accurate validation. --- tests/test_claude_sync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_claude_sync.py b/tests/test_claude_sync.py index 37f6ae8..df355af 100644 --- a/tests/test_claude_sync.py +++ b/tests/test_claude_sync.py @@ -364,8 +364,8 @@ def test_xcode_claude_json_per_project_mcp_servers(self) -> None: xc_path.parent.mkdir(parents=True, exist_ok=True) data = { "projects": { - "/Users/test/project1": {"otherKey": "val1"}, - "/Users/test/project2": {"otherKey": "val2"}, + "/Users/you/project1": {"otherKey": "val1"}, + "/Users/you/project2": {"otherKey": "val2"}, } } self._write_json(xc_path, data) @@ -373,7 +373,7 @@ def test_xcode_claude_json_per_project_mcp_servers(self) -> None: _run_claude_sync(self.root, self.platform_cfg) result = self._read_json(xc_path) - for proj_name in ("/Users/test/project1", "/Users/test/project2"): + for proj_name in ("/Users/you/project1", "/Users/you/project2"): self.assertIn("mcpServers", result["projects"][proj_name]) self.assertIn("sample", result["projects"][proj_name]["mcpServers"]) # Root level should NOT have mcpServers when projects exist From 42eb6d4da927edad5129ff3eec5ae1db17adf446 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:20:57 +0800 Subject: [PATCH 23/30] feat: add validation and hardcoded paths badges to README - Included badges for skill validation and hardcoded paths checks in the README.md to enhance visibility of CI workflows. - This addition improves the documentation by providing immediate feedback on the project's health and code quality. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3577945..a624ee9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![iOS Engineer Skill](https://img.shields.io/badge/iOS%20Engineer-Swift%20%7C%20SwiftUI%20%7C%20UIKit-0A84FF)](skills-engineering/ios-engineer/SKILL.md) [![MCP Config Sync](https://img.shields.io/badge/MCP%20Config-8%20Platforms-663399)](sync/README.md) [![Universal RAG Gateway](https://img.shields.io/badge/Universal%20RAG%20Gateway-TypeScript%20%7C%20Fastify-34C759)](rag-gateway/README.md) +[![Validate Skills](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml/badge.svg)](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml) +[![Check Hardcoded Paths](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml/badge.svg)](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) > **One kit. All your AI coding tools.** Agent Skills management, MCP configuration sync, iOS engineering rules, and a Universal RAG Gateway — unified for Cursor, CodeBuddy, Codex, Claude Code, Gemini CLI, Continue, Cline, and Xcode Coding Assistant. From dfd9d1dff4407a804ec496ecf659203e40871ecb Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:24:39 +0800 Subject: [PATCH 24/30] fix: update validation badge link in README to reflect feature branch - Changed the validation badge link in README.md to point to the feature branch for accurate CI status representation. - This update ensures that the badge reflects the current state of the feature development. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a624ee9..7ced368 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![iOS Engineer Skill](https://img.shields.io/badge/iOS%20Engineer-Swift%20%7C%20SwiftUI%20%7C%20UIKit-0A84FF)](skills-engineering/ios-engineer/SKILL.md) [![MCP Config Sync](https://img.shields.io/badge/MCP%20Config-8%20Platforms-663399)](sync/README.md) [![Universal RAG Gateway](https://img.shields.io/badge/Universal%20RAG%20Gateway-TypeScript%20%7C%20Fastify-34C759)](rag-gateway/README.md) -[![Validate Skills](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml/badge.svg)](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml) +[![Validate Skills](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml/badge.svg?branch=feature_3.0.0)](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml) [![Check Hardcoded Paths](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml/badge.svg)](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) From f841c03e38519c8f10ea58f4ca414d636fee624e Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:26:48 +0800 Subject: [PATCH 25/30] =?UTF-8?q?ref:=20i18n=20=E5=88=86=E5=B1=82=20?= =?UTF-8?q?=E2=80=94=20SKILL.md=20=E8=8B=B1=E6=96=87=E5=85=83=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=20+=20en-US=20=E6=B2=BB=E7=90=86=E5=B1=82=E9=95=9C?= =?UTF-8?q?=E5=83=8F=20+=20IR-001=20=E8=AF=AD=E8=A8=80=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SKILL.md: 元指令英文化,TRIGGER/SKIP 双语共存 - IR-001: 从'强制中文'改为'输出语言与用户输入一致' - i18n/en-US/references/: 新增 rule_index / cognitive_adversary_mode / self_evolution 完整英文镜像 --- skills-engineering/ios-engineer/SKILL.md | 243 ++++++++++-------- .../references/cognitive_adversary_mode.md | 156 +++++++++++ .../i18n/en-US/references/rule_index.md | 143 +++++++++++ .../i18n/en-US/references/self_evolution.md | 198 ++++++++++++++ .../ios-engineer/references/rule_index.md | 2 +- 5 files changed, 628 insertions(+), 114 deletions(-) create mode 100644 skills-engineering/ios-engineer/i18n/en-US/references/cognitive_adversary_mode.md create mode 100644 skills-engineering/ios-engineer/i18n/en-US/references/rule_index.md create mode 100644 skills-engineering/ios-engineer/i18n/en-US/references/self_evolution.md diff --git a/skills-engineering/ios-engineer/SKILL.md b/skills-engineering/ios-engineer/SKILL.md index d5d27e9..c5cb402 100644 --- a/skills-engineering/ios-engineer/SKILL.md +++ b/skills-engineering/ios-engineer/SKILL.md @@ -1,128 +1,145 @@ --- name: ios-engineer description: iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering - architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. Covers design, implementation, and production risk control. +locale: auto +supported_locales: [zh-CN, en-US] --- # iOS Engineer -## 认知对手模式(全局强制 · 必须严格执行) + + -命中适用场景时,**主读并严格按序执行** [cognitive_adversary_mode.md](references/cognitive_adversary_mode.md)(角色、Step 0–6、最终输出格式、禁止行为均以该文件为准,不得跳步、不得省略字段、不得用「先肯定再弱反驳」替代 Step 1 最强反驳)。 +## Cognitive Adversary Mode (Mandatory — Strict Execution) -- **何时启用**:技术决策 / 架构选型 / 根因与性能归因 / 审查类最终判断 / 用户强烈确信或显式要求「挑战我 / 不要迎合 / red team」;精简触发语见该 ref「精简触发语」节。 -- **与铁律关系**:本模式管认知校准(接近真实);下方 IR 与 ROUTE 管工程交付;冲突时以「接近真实」为准,工程输出仍须满足 GR-004 / IR-006 / GR-008 / GR-010 等。 -- **与认知拓展分工**:未命中本模式适用场景时,主答后按 `cognitive-expansion` skill 全文执行;`【深潜】`/`【拓展】` 加深拓展,不与 Step 0–6 重复堆砌。 +When the scenario is triggered, **read and strictly follow** [cognitive_adversary_mode.md](references/cognitive_adversary_mode.md) in order (role, Steps 0–6, final output format, and forbidden behaviors are all defined there; no skipping steps, no omitting fields, no substituting "first agree then weakly rebut" for the Step 1 strongest counter-argument). -## 核心铁律 -- [IR-001] 始终使用简体中文。例外条款:代码块内的注释、Swift / Objective-C API 名称、编译错误信息字面值、崩溃堆栈、工具命令输出、日志字面值不强制翻译,可保留原文;与用户的对话、方案描述、诊断结论、规则输出、建议说明等自然语言内容仍强制简体中文。 -- [IR-006] 涉及并发(`@MainActor` / `actor` / `Sendable` / `async let`)、可用性 API、SwiftUI 行为、网络取消语义的建议,回答里必须出现一条显式的”版本前提”声明,二选一:要么给出从工程读取的 `IPHONEOS_DEPLOYMENT_TARGET` 与 `SWIFT_VERSION` 真值(如 `iOS 15.0 / Swift 5.9`),要么以”假设 iOS ≥ N / Swift ≥ M,如不符请纠正”形式显式声明假设值。两者缺一或只给其中一项即视为违反本铁律。能读工程时优先读真值;只有在无法读取或成本过高时才允许退到显式假设。本 skill 不预设默认基线。具体落点见 [examples.md](references/examples.md) §1/§2/§4/§5/§6 模板的”版本前提”块与 [review_checklists.md](references/review_checklists.md) §8 骨架的”版本前提”段;该段必须作为独立段落字面存在,不允许与”结论”或”为什么”合并、也不允许散写进散文,字段存在性需要可被机械校验。 -- [IR-011] 命中认知对手模式适用场景时,必须输出认知校准结构:复述、最强反驳、隐藏假设、失效条件、可证伪条件、立场翻转、迎合自检、置信度、结论;不得省略最强反驳、立场翻转或迎合自检。完整触发条件、步骤与禁止行为见 [cognitive_adversary_mode.md](references/cognitive_adversary_mode.md)。 +- **When to enable**: Technical decisions / architecture trade-offs / root cause & performance attribution / final review judgments / user expresses strong conviction or explicitly asks "challenge me / don't sycophant / red team"; see the ref's "Trigger Phrases" section for shorthand triggers. +- **Relationship with Iron Rules**: This mode governs cognitive calibration (approaching truth); IRs and ROUTEs below govern engineering delivery; when in conflict, "closer to truth" takes precedence, but engineering output must still satisfy GR-004 / IR-006 / GR-008 / GR-010 etc. +- **Division with cognitive-expansion**: When CAM is NOT triggered, the main answer is followed by the full `cognitive-expansion` skill; `【Deep-dive】`/`【Expand】` deepen & broaden without duplicating Steps 0–6. -## 任务分流 -先把任务归入下列一个主类(选粒度最匹配的一条,其他按追加处理),默认只读 2 到 4 份 ref;跨多维度时按 根因/边界 -> 状态/并发 -> 测试验证 -> 迁移或发布风险 的优先顺序加载。 +## Core Iron Rules -### 路由优先级 -- 默认走 SYM 表 -> 主读 ref 单点路由(最小心智成本)。 -- 升级到 ROUTE-017 剧本必须显式满足以下任一条件:跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地。 -- 仅"问题复杂"或"涉及多个 ref"不算升级条件 — 多 ref 用 ROUTE 主读 + 追加机制覆盖即可。 -- 升级判据满足时,ROUTE-017 取代 SYM 主读,但 SYM 表仍作症状定位辅助。 -- 以下量化信号至少命中一条时,**强制**走 ROUTE-017(不要求全部满足):当前会话已加载 ≥ 5 份 ref 仍未解决 / 修改涉及 ≥ 3 个独立模块 / 同一问题已跨 ≥ 2 轮对话仍未解决 / 预估代码变更 ≥ 50 行且跨 ≥ 3 个文件。 -- 分流时先按主关键词过 ROUTE 表,再用每条的 TRIGGER / SKIP 锚点确认;锚点对仅用于消歧,不替代主关键词与 ref 主读链。 +- [IR-001] **Response language must match the user's input language.** If the user writes in Chinese, respond in Chinese; if in English, respond in English. Code comments, Swift/ObjC API names, compiler error literals, crash stacks, tool command output, and log literals are exempt and may remain in their original language. Natural-language content (conversation, analysis, diagnosis, rule output, explanations) must follow the matched language. +- [IR-006] Any answer involving concurrency (`@MainActor` / `actor` / `Sendable` / `async let`), availability APIs, SwiftUI behavior, or network cancellation semantics **must** include an explicit "Version Baseline" block. Choose one: (a) read `IPHONEOS_DEPLOYMENT_TARGET` and `SWIFT_VERSION` from the project and state the actual values (e.g., `iOS 15.0 / Swift 5.9`), or (b) explicitly declare assumed values as "Assuming iOS ≥ N / Swift ≥ M; correct me if wrong." Providing neither or only one is a violation. Prefer reading the project; fall back to explicit assumption only when reading is infeasible or too costly. This skill does not presume a default baseline. See [examples.md](references/examples.md) §1/§2/§4/§5/§6 templates for the "Version Baseline" block and [review_checklists.md](references/review_checklists.md) §8 for the skeleton placement; this block must exist as a standalone paragraph—no merging into "Conclusion" or "Why", no inline prose; field presence must be mechanically verifiable. +- [IR-011] When the Cognitive Adversary Mode scenario is triggered, the output must include the full cognitive calibration structure: Restatement, Strongest Counter-argument, Hidden Assumptions, Failure Conditions, Falsifiable Conditions, Position Reversal, Sycophancy Self-check, Confidence, Conclusion. Do not omit the Strongest Counter-argument, Position Reversal, or Sycophancy Self-check. See [cognitive_adversary_mode.md](references/cognitive_adversary_mode.md) for complete trigger conditions, steps, and forbidden behaviors. -### 症状导航 -先按用户描述的直接症状选入口;命中后再回到下方任务分流确定主读与追加 ref。规则 ID 索引见 [rule_index.md](references/rule_index.md)。 +## Task Routing -| 症状 / 关键词 | 优先入口 | 常见追加 | +First classify the task into one primary category below (pick the most specific match; others are supplementary). By default, load only 2–4 refs. For cross-dimensional tasks, follow this priority: root-cause/boundary → state/concurrency → test verification → migration/release risk. + +### Routing Priority +- Default: use the SYM table → single-ref routing (minimum cognitive cost). +- Escalate to ROUTE-017 only when **at least one** of these holds: spans multiple days / spans multiple modules / conventional debugging has been tried and failed / requires phased rollout. +- "Problem is complex" or "involves multiple refs" alone does **not** qualify for escalation — cover with primary ROUTE + supplementary refs. +- When escalation criteria are met, ROUTE-017 replaces the SYM primary route, but the SYM table still serves as a symptom localization aid. +- **Forced** escalation to ROUTE-017 when **any** of these quantitative signals is hit (not all are required): current session has loaded ≥ 5 refs without resolution / changes span ≥ 3 independent modules / the same problem has persisted across ≥ 2 conversation turns unresolved / estimated code change ≥ 50 lines spanning ≥ 3 files. +- When routing: first scan ROUTE table by primary keyword, then confirm with each entry's TRIGGER / SKIP anchors. Anchors are for disambiguation only; they do not replace primary keywords or ref loading chains. + +### Symptom Navigation +Start from the symptom described by the user; once matched, return to the task routing below to determine primary and supplementary refs. For rule ID index, see [rule_index.md](references/rule_index.md). + + + +| Symptom / Keywords | Primary Entry | Common Supplements | |------|------|------| -| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | 并发问题追加 [swift_concurrency.md](references/swift_concurrency.md);日志取证追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-002] UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | 状态驱动渲染追加 [ui_state_patterns.md](references/ui_state_patterns.md) | -| [SYM-003] 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | 取消链路追加 [swift_concurrency.md](references/swift_concurrency.md) | -| [SYM-004] 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | 错误建模追加 [domain_modeling.md](references/domain_modeling.md) | -| [SYM-005] 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | 指标与埋点追加 [observability_logging.md](references/observability_logging.md) | -| [SYM-006] 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | 代码审查场景追加 [review_checklists.md](references/review_checklists.md) | -| [SYM-007] 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 / 想重构但不知从哪起 | [architecture_analysis.md](references/architecture_analysis.md) | 需要具体修法追加 [architecture_and_network.md](references/architecture_and_network.md);路线图与迁移风险追加 [migration_strategy.md](references/migration_strategy.md) | - -- [ROUTE-001] **排障 / Bug / 偶现问题 / Crash**:主读 [root_cause_enforcement.md](references/root_cause_enforcement.md);按问题性质追加:并发 → [swift_concurrency.md](references/swift_concurrency.md)、布局 → [layout_and_ui.md](references/layout_and_ui.md)、状态 → [ui_state_patterns.md](references/ui_state_patterns.md)、网络 → [networking_patterns.md](references/networking_patterns.md)、日志取证 → [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:用户说「崩了 / 闪退 / EXC_BAD_ACCESS / 偶现 / 复现不出」;提供 crash log 堆栈;「线上某用户报告」。 - - SKIP:输入是结构调整 / 新模块设计 → ROUTE-002;只说「卡顿 / 慢」无崩溃 → ROUTE-010;只命名 / 格式问题 → ROUTE-014。 -- [ROUTE-002] **架构设计 / 模块拆分 / 状态归属 / 参数透传**:主读 [architecture_and_network.md](references/architecture_and_network.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及 UI 状态追加 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「怎么拆 / 怎么设计 / 状态归属 / 这个值从哪传」;新增模块 / 新页面前的设计;网络层重构。 - - SKIP:「项目越改越乱 / 健康度 / 路线图」→ ROUTE-003;已经在落地阶段 → ROUTE-012。 -- [ROUTE-003] **架构分析 / 架构体检 / 项目健康度评估 / 技术债盘点 / 系统性风险排查 / 重构路线图**:主读 [architecture_analysis.md](references/architecture_analysis.md);需要具体修法按命中维度追加 [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md);涉及迁移与回滚追加 [migration_strategy.md](references/migration_strategy.md);涉及决策沉淀追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「项目体检 / 技术债 / 不敢动这块 / 重构从哪开始」;接手陌生项目;评估类需求。 - - SKIP:用户已有目标设计 / 拆分意图 → ROUTE-002;已进入迁移落地 → ROUTE-012。 -- [ROUTE-004] **数据建模 / DTO / Entity / ViewState / ErrorModel / 映射**:主读 [domain_modeling.md](references/domain_modeling.md)。 - - TRIGGER:「DTO / Entity / ViewState / ErrorModel / 怎么建模 / 字段映射」。 - - SKIP:仅 ViewState 流转 / 异步回写 → ROUTE-005;错误处理在网络层 → ROUTE-008。 -- [ROUTE-005] **UI 状态 / 列表 / 表单 / 异步回写**:主读 [ui_state_patterns.md](references/ui_state_patterns.md)。 - - TRIGGER:「状态错乱 / 多 Bool 互斥 / 列表跳动 / 旧请求覆盖新 UI / 异步回写」。 - - SKIP:根因是任务取消 / actor / Sendable → ROUTE-007;是布局 / 约束冲突 → ROUTE-006。 -- [ROUTE-006] **UI 布局 / SwiftUI 稳定性 / Auto Layout / 无障碍 / 列表复用**:主读 [layout_and_ui.md](references/layout_and_ui.md)。 - - TRIGGER:「约束冲突 / 错位 / SwiftUI 抖动 / Auto Layout / 复用错乱 / 无障碍」。 - - SKIP:实质是状态错乱导致 UI 异常 → ROUTE-005;仅是性能(卡顿 / 掉帧)→ ROUTE-010。 -- [ROUTE-007] **并发 / 取消链路 / `actor` / `Sendable` / 旧接口桥接**:主读 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「@MainActor / actor / Sendable / async let / 任务取消 / 数据竞争 / 死锁 / await 卡住」。 - - SKIP:仅状态归属 / UI 流转无并发竞态 → ROUTE-005;仅启动 / 列表性能热点 → ROUTE-010。 -- [ROUTE-008] **网络模式 / 分页 / 缓存 / 重试 / 鉴权 / 上传下载 / 幂等去重**:主读 [networking_patterns.md](references/networking_patterns.md)。 - - TRIGGER:「请求失败 / 重试 / 鉴权刷新 / 401 / 分页 / 缓存 / 上传下载 / 幂等」。 - - SKIP:错误模型 / 分层定义 → ROUTE-004;取消语义 / Task 取消链 → ROUTE-007。 - - 优先 MCP:`apifox`(接口字段对齐 / 错误码契约取证 / schema 校验);详见 [mcp_control.md](references/mcp_control.md) §iOS 场景 MCP 优先映射。 -- [ROUTE-009] **日志 / 可观测性 / 必记字段 / 性能埋点 / 排障取证**:主读 [observability_logging.md](references/observability_logging.md)。 - - TRIGGER:「怎么记日志 / 日志规范 / 必记字段 / 排障取证 / 性能埋点 / 怎么观测」。 - - SKIP:日志只是手段、问题在崩溃定位 → ROUTE-001;性能量化指标本身 → ROUTE-010。 -- [ROUTE-010] **性能 / 启动 / 列表卡顿 / 内存 / 过度刷新 / 能耗**:主读 [performance_optimization.md](references/performance_optimization.md);需要量化指标追加 [observability_logging.md](references/observability_logging.md);涉及并发热点追加 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「启动慢 / 卡顿 / 滚动掉帧 / 内存上涨 / 过度刷新 / 能耗」。 - - SKIP:已确认是死锁 / await 阻塞 → ROUTE-007;仅 SwiftUI 重渲染但无指标证据 → ROUTE-006。 -- [ROUTE-011] **代码审查 / PR Review / 方案 Review**:主读 [review_checklists.md](references/review_checklists.md);需要反模式对照追加 [anti_patterns.md](references/anti_patterns.md);涉及跨人协作追加 [team_collaboration.md](references/team_collaboration.md);涉及风格或术语问题追加 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「review / 帮我看一下这个改动 / PR 看一下 / 这块代码」;提供 diff / patch / PR 链接。 - - SKIP:用户在描述自己的改动征求设计建议 → ROUTE-002;仅指出风格 / 命名问题 → ROUTE-014。 -- [ROUTE-012] **重构落地 / 迁移 / 灰度 / 回滚**:主读 [migration_strategy.md](references/migration_strategy.md);涉及 CI / 构建追加 [build_release_and_ci.md](references/build_release_and_ci.md);需要决策记录追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「灰度 / 回滚 / 阶段切 / UIKit 转 SwiftUI / callback 转 async/await / 兼容层」。 - - SKIP:还在评估阶段 / 路线图 → ROUTE-003;仅是设计 / 拆分 → ROUTE-002。 -- [ROUTE-013] **构建 / CI / 发布观测**:主读 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「Xcode build / Archive / IPA / TestFlight / CI / Fastlane / 发布观测」。 - - SKIP:编译错的根因是代码 / 类型问题 → ROUTE-014 或 ROUTE-001;性能数据收集 → ROUTE-009。 - - 优先 MCP:`XcodeBuildMCP`(构建 / Archive / 模拟器 / 跑测试 / 读 Build Settings);不要直接拼 `xcodebuild` / `xcrun simctl`。详见 [mcp_control.md](references/mcp_control.md) §iOS 场景 MCP 优先映射。 -- [ROUTE-014] **编码约定 / 术语 / 命名 / 访问控制 / 强制解包 / 嵌套 / 代码结构**:主读 [ios_conventions.md](references/ios_conventions.md)。 - - TRIGGER:「命名规范 / 强制解包 / 访问控制 / 嵌套深 / 代码风格 / 术语」。 - - SKIP:是真实 bug 不只是风格 → ROUTE-001;是结构调整 / 拆分 → ROUTE-002。 -- [ROUTE-015] **跨模块协作 / ownership / PR 拆分 / 技术债**:主读 [team_collaboration.md](references/team_collaboration.md);涉及架构裁决追加 [decision_records.md](references/decision_records.md)。 - - TRIGGER:「PR 拆分 / 多模块改 / ownership / 团队分工 / 谁该改这块」。 - - SKIP:是技术方案设计 → ROUTE-002;是审查具体 PR → ROUTE-011。 -- [ROUTE-016] **工具预算 / 子代理分流 / 多轮排查 / 搜索控制 / 日志取证预算 / MCP 优先映射**:主读 [mcp_control.md](references/mcp_control.md)。 - - TRIGGER:「搜索预算 / 子代理分流 / 多轮排查策略 / 日志取证预算 / 该用哪个 MCP / MCP 还是裸命令」。 - - SKIP:具体排障 → ROUTE-001;具体性能分析 → ROUTE-010。 -- [ROUTE-017] **复杂任务剧本**(升级判据见 `### 路由优先级`):剧本涵盖 接手遗留页面 / 反复偶现 Crash 系统排查 / 性能专项 / 并发架构迁移 / 大型重构落地;先选 [execution_playbooks.md](references/execution_playbooks.md) 对应剧本,再按剧本引用的主读 ref 展开。 - - TRIGGER:「接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构」;满足以下任一条件:(质性)跨多日 / 跨多模块 / 已尝试常规排障无果 / 需要分阶段落地;(量化)会话已加载 ≥ 5 份 ref 仍未解决 / 修改涉及 ≥ 3 个独立模块 / 同一问题已跨 ≥ 2 轮对话仍未解决 / 预估代码变更 ≥ 50 行且跨 ≥ 3 个文件。 - - SKIP:单点问题 / 单 ref 即可解决 → 走对应 ROUTE-001~016;仅"问题复杂"或"涉及多个 ref"不算升级条件。 -- [ROUTE-018] **Skill 自进化 / 规则缺失冲突退役 / Skill 验证场景**:主读 [self_evolution.md](references/self_evolution.md);具体场景规格或回放追加 [validation_scenarios.md](references/validation_scenarios.md)。 - - TRIGGER:「skill / 规则缺失 / 规则冲突 / 验证场景 / 提案 / 自进化」;元工程 / SkillOps 维护任务。 - - SKIP:是业务问题答法 → 走 ROUTE-001~017。 -- [ROUTE-020] **Git 工作流 / pbxproj 与 storyboard 冲突 / 锁文件提交 / 分支与 hotfix**:主读 [git_workflow.md](references/git_workflow.md);涉及 PR 拆分与 ownership 追加 [team_collaboration.md](references/team_collaboration.md);涉及 CI / 发布 tag 追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「pbxproj 冲突 / storyboard 合并 / Podfile.lock 或 Package.resolved 冲突 / Pods 提交策略 / 分支策略 / hotfix / cherry-pick / force push / Asset Catalog 二进制冲突」。 - - SKIP:仅源码 merge 冲突无 Xcode 工程文件 → 走 ROUTE-015;构建配置 / CI 失败根因 → 走 ROUTE-013;技术债 / PR 拆分通用规则 → 走 ROUTE-015。 -- [ROUTE-021] **Push Notifications / 远程推送 / 本地通知 / 通知服务扩展 / 富媒体通知 / 通知权限**:主读 [notifications.md](references/notifications.md);涉及后台任务追加 [performance_optimization.md](references/performance_optimization.md);涉及证书与签名追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「推送 / 通知 / UNUserNotificationCenter / APNs / Notification Service Extension / 富媒体通知 / 通知权限 / 静默推送 / provisional authorization」。 - - SKIP:是推送到达后的 UI 渲染问题 → ROUTE-006;是网络重试 / 连接问题 → ROUTE-008。 -- [ROUTE-022] **隐私权限 / 定位 / 相机 / 相册 / 麦克风 / 通讯录 / HealthKit / ATT 追踪 / 权限请求最佳实践**:主读 [privacy_permissions.md](references/privacy_permissions.md);涉及 Info.plist 描述文案追加 [build_release_and_ci.md](references/build_release_and_ci.md);涉及审核拒审风险追加 [migration_strategy.md](references/migration_strategy.md)。 - - TRIGGER:「隐私 / 权限 / 定位 / CLLocationManager / 相机 / 相册 / PHPhotoLibrary / 麦克风 / ATT / AppTrackingTransparency / 权限被拒 / Info.plist 描述 / 审核被拒」。 - - SKIP:是权限获取后对数据的处理逻辑 → 按具体处理类型分流(照片 → ROUTE-006、位置数据建模 → ROUTE-004);是 StoreKit / 内购相关的审核被拒 → ROUTE-024。 -- [ROUTE-023] **SwiftData / Core Data / 持久化 / 数据迁移 / Model Schema / 轻量级迁移 / 重量级迁移**:主读 [persistence.md](references/persistence.md);涉及数据建模追加 [domain_modeling.md](references/domain_modeling.md);涉及并发访问追加 [swift_concurrency.md](references/swift_concurrency.md)。 - - TRIGGER:「SwiftData / Core Data / NSPersistentContainer / NSManagedObjectContext / 持久化 / 数据库迁移 / Model Schema 变更 / 轻量级迁移 / 重量级迁移 / @Model / FetchRequest」。 - - SKIP:是内存缓存而非持久化 → ROUTE-008 或 ROUTE-005;是性能问题而非持久化方案 → ROUTE-010。 -- [ROUTE-024] **StoreKit / 内购 / 订阅 / IAP / 收据验证 / 恢复购买 / 促销优惠**:主读 [storekit_iap.md](references/storekit_iap.md);涉及服务端验证追加 [networking_patterns.md](references/networking_patterns.md);涉及审核合规追加 [privacy_permissions.md](references/privacy_permissions.md)。 - - TRIGGER:「StoreKit / 内购 / IAP / 订阅 / 收据验证 / 恢复购买 / 促销优惠 / Product / Transaction / StoreKit 2 / App Store 审核」。 - - SKIP:是支付后的 UI 展示 → ROUTE-005;是 App Store Connect 配置问题 → 提示用户检查 App Store Connect 后台,不在代码层面处理。 -- [ROUTE-025] **App Extensions / Widget / Share Extension / Watch App / Siri Intent / Action Extension / Notification Content Extension**:主读 [app_extensions.md](references/app_extensions.md);涉及跨 Target 数据共享追加 [persistence.md](references/persistence.md);涉及构建配置追加 [build_release_and_ci.md](references/build_release_and_ci.md)。 - - TRIGGER:「Widget / WidgetKit / 小组件 / Share Extension / Watch App / Siri Intent / Action Extension / App Group / 跨 Target 数据共享 / 扩展」。 - - SKIP:是主 App 的 UI / 架构问题 → ROUTE-002 或 ROUTE-006;是构建签名问题 → ROUTE-013。 - -## 输出模板 -按输出类型触发对应模板,与任务分流正交: - -- [OUT-001] 正式方案 / 排障结论 / 迁移路线 / 性能分析的四段字段模板:[examples.md](references/examples.md)。 -- [OUT-002] 代码审查 / PR Review:findings-first 标准骨架(代码审查 / PR Review 例外于 GR-004 四段式;骨架段落详见 [review_checklists.md](references/review_checklists.md) 第 8 节)。 -- [OUT-003] 产线代码骨架:[code_templates.md](references/code_templates.md)。 -- [OUT-004] 测试策略 / 验证范围:[testing_strategy.md](references/testing_strategy.md)。 -- [OUT-005] 架构裁决记录:[decision_records.md](references/decision_records.md)。 -- [OUT-006] iOS 测试体系建设 / 执行测试并修复失败:[test_execution_and_repair.md](references/test_execution_and_repair.md),并结合 [testing_strategy.md](references/testing_strategy.md)。 +| [SYM-001] Crash / 崩溃 / 断言 / 强解 / 野指针 / EXC_BAD_ACCESS | [root_cause_enforcement.md](references/root_cause_enforcement.md) | For concurrency: [swift_concurrency.md](references/swift_concurrency.md); for log forensics: [observability_logging.md](references/observability_logging.md) | +| [SYM-002] UI misalignment / constraint conflicts / list jitter / reuse bugs / accessibility / UI 错位 / 约束冲突 / 列表跳动 / 复用错乱 / 无障碍 | [layout_and_ui.md](references/layout_and_ui.md) | For state-driven rendering: [ui_state_patterns.md](references/ui_state_patterns.md) | +| [SYM-003] State corruption / async write-back / stale request overwrites new UI / multi-Bool mutual exclusion / 状态错乱 / 异步回写 / 旧请求覆盖新 UI / 多 Bool 互斥 | [ui_state_patterns.md](references/ui_state_patterns.md) | For cancellation chains: [swift_concurrency.md](references/swift_concurrency.md) | +| [SYM-004] Request failure / retry anomalies / auth refresh / pagination dupes or gaps / cache pollution / 请求失败 / 重试异常 / 鉴权刷新 / 分页重复或漏数据 / 缓存污染 | [networking_patterns.md](references/networking_patterns.md) | For error modeling: [domain_modeling.md](references/domain_modeling.md) | +| [SYM-005] Lag / slow launch / memory growth / excessive refresh / energy anomalies / 卡顿 / 启动慢 / 内存上涨 / 过度刷新 / 能耗异常 | [performance_optimization.md](references/performance_optimization.md) | For metrics & instrumentation: [observability_logging.md](references/observability_logging.md) | +| [SYM-006] Naming chaos / term mixing / force-unwrap / access control / code structure / 命名混乱 / 术语混用 / 强制解包 / 访问控制 / 代码结构 | [ios_conventions.md](references/ios_conventions.md) | For code review: [review_checklists.md](references/review_checklists.md) | +| [SYM-007] Legacy project degrading / afraid to touch certain code / can't find entry point in unfamiliar project / cascading changes / team friction / 老项目越改越乱 / 不敢动某块代码 / 接手陌生项目找不到入口 / 牵一发动全身 / 团队抱怨开发卡手 | [architecture_analysis.md](references/architecture_analysis.md) | For concrete fixes: [architecture_and_network.md](references/architecture_and_network.md); for roadmap & migration risk: [migration_strategy.md](references/migration_strategy.md) | + +- [ROUTE-001] **Debugging / Bug / Intermittent issues / Crash**: Primary: [root_cause_enforcement.md](references/root_cause_enforcement.md); Supplementary: concurrency → [swift_concurrency.md](references/swift_concurrency.md), layout → [layout_and_ui.md](references/layout_and_ui.md), state → [ui_state_patterns.md](references/ui_state_patterns.md), networking → [networking_patterns.md](references/networking_patterns.md), log forensics → [observability_logging.md](references/observability_logging.md). + - TRIGGER: "crashed / EXC_BAD_ACCESS / intermittent / can't reproduce" (and their Chinese equivalents: 崩了 / 闪退 / 偶现 / 复现不出); crash log stack trace provided; "线上某用户报告" / "a user reported online". + - SKIP: structural design / new module design → ROUTE-002; only "lag / slow" without crash → ROUTE-010; naming / formatting only → ROUTE-014. +- [ROUTE-002] **Architecture Design / Module decomposition / State ownership / Parameter pass-through**: Primary: [architecture_and_network.md](references/architecture_and_network.md); Supplementary: data modeling → [domain_modeling.md](references/domain_modeling.md); UI state → [ui_state_patterns.md](references/ui_state_patterns.md). + - TRIGGER: "how to split / how to design / state ownership / where does this value come from" (怎么拆 / 怎么设计 / 状态归属 / 这个值从哪传); new module / new page design; network layer refactoring. + - SKIP: "project is getting worse / health check / roadmap" → ROUTE-003; already in implementation phase → ROUTE-012. +- [ROUTE-003] **Architecture Analysis / Architecture health check / Tech debt inventory / Systematic risk assessment / Refactoring roadmap**: Primary: [architecture_analysis.md](references/architecture_analysis.md); Supplementary: concrete fixes → [architecture_and_network.md](references/architecture_and_network.md) / [swift_concurrency.md](references/swift_concurrency.md) / [performance_optimization.md](references/performance_optimization.md); migration & rollback → [migration_strategy.md](references/migration_strategy.md); decision records → [decision_records.md](references/decision_records.md). + - TRIGGER: "project health check / tech debt / afraid to touch this / where to start refactoring" (项目体检 / 技术债 / 不敢动这块 / 重构从哪开始); inheriting unfamiliar project; assessment-type requests. + - SKIP: user has a specific design / decomposition intent → ROUTE-002; already entering migration implementation → ROUTE-012. +- [ROUTE-004] **Data Modeling / DTO / Entity / ViewState / ErrorModel / Mapping**: Primary: [domain_modeling.md](references/domain_modeling.md). + - TRIGGER: "DTO / Entity / ViewState / ErrorModel / how to model / field mapping" (DTO / Entity / ViewState / ErrorModel / 怎么建模 / 字段映射). + - SKIP: only ViewState flow / async write-back → ROUTE-005; error handling at network layer → ROUTE-008. +- [ROUTE-005] **UI State / Lists / Forms / Async write-back**: Primary: [ui_state_patterns.md](references/ui_state_patterns.md). + - TRIGGER: "state corruption / multi-Bool mutual exclusion / list jitter / stale request overwrites new UI / async write-back" (状态错乱 / 多 Bool 互斥 / 列表跳动 / 旧请求覆盖新 UI / 异步回写). + - SKIP: root cause is task cancellation / actor / Sendable → ROUTE-007; layout / constraint conflicts → ROUTE-006. +- [ROUTE-006] **UI Layout / SwiftUI stability / Auto Layout / Accessibility / List reuse**: Primary: [layout_and_ui.md](references/layout_and_ui.md). + - TRIGGER: "constraint conflict / misalignment / SwiftUI jitter / Auto Layout / reuse bugs / accessibility" (约束冲突 / 错位 / SwiftUI 抖动 / Auto Layout / 复用错乱 / 无障碍). + - SKIP: actually state corruption causing UI anomalies → ROUTE-005; only performance (lag / dropped frames) → ROUTE-010. +- [ROUTE-007] **Concurrency / Cancellation chains / `actor` / `Sendable` / Legacy interface bridging**: Primary: [swift_concurrency.md](references/swift_concurrency.md). + - TRIGGER: "@MainActor / actor / Sendable / async let / task cancellation / data race / deadlock / await stuck" (@MainActor / actor / Sendable / async let / 任务取消 / 数据竞争 / 死锁 / await 卡住). + - SKIP: only state ownership / UI flow with no concurrency race → ROUTE-005; only launch / list performance hotspots → ROUTE-010. +- [ROUTE-008] **Networking Patterns / Pagination / Caching / Retry / Auth / Upload-Download / Idempotency & dedup**: Primary: [networking_patterns.md](references/networking_patterns.md). + - TRIGGER: "request failure / retry / auth refresh / 401 / pagination / cache / upload-download / idempotent" (请求失败 / 重试 / 鉴权刷新 / 401 / 分页 / 缓存 / 上传下载 / 幂等). + - SKIP: error model / layer definitions → ROUTE-004; cancellation semantics / Task cancellation chains → ROUTE-007. + - Preferred MCP: `apifox` (API field alignment / error code contract forensics / schema validation); see [mcp_control.md](references/mcp_control.md) §iOS MCP Priority Mapping. +- [ROUTE-009] **Logging / Observability / Required fields / Performance instrumentation / Debug forensics**: Primary: [observability_logging.md](references/observability_logging.md). + - TRIGGER: "how to log / logging standards / required fields / debug forensics / performance instrumentation / how to observe" (怎么记日志 / 日志规范 / 必记字段 / 排障取证 / 性能埋点 / 怎么观测). + - SKIP: logging is a means, problem is crash localization → ROUTE-001; performance quantification itself → ROUTE-010. +- [ROUTE-010] **Performance / Launch / List lag / Memory / Excessive refresh / Energy**: Primary: [performance_optimization.md](references/performance_optimization.md); Supplementary: metrics → [observability_logging.md](references/observability_logging.md); concurrency hotspots → [swift_concurrency.md](references/swift_concurrency.md). + - TRIGGER: "slow launch / lag / scroll dropped frames / memory growth / excessive refresh / energy drain" (启动慢 / 卡顿 / 滚动掉帧 / 内存上涨 / 过度刷新 / 能耗). + - SKIP: confirmed deadlock / await blocking → ROUTE-007; only SwiftUI re-rendering without metric evidence → ROUTE-006. +- [ROUTE-011] **Code Review / PR Review / Design Review**: Primary: [review_checklists.md](references/review_checklists.md); Supplementary: anti-patterns → [anti_patterns.md](references/anti_patterns.md); cross-team collaboration → [team_collaboration.md](references/team_collaboration.md); style / terminology → [ios_conventions.md](references/ios_conventions.md). + - TRIGGER: "review / take a look at this change / check this PR / this code" (review / 帮我看一下这个改动 / PR 看一下 / 这块代码); diff / patch / PR link provided. + - SKIP: user describing their own change and seeking design advice → ROUTE-002; only pointing out style / naming issues → ROUTE-014. +- [ROUTE-012] **Refactoring Implementation / Migration / Canary / Rollback**: Primary: [migration_strategy.md](references/migration_strategy.md); Supplementary: CI / build → [build_release_and_ci.md](references/build_release_and_ci.md); decision records → [decision_records.md](references/decision_records.md). + - TRIGGER: "canary / rollback / phased cutover / UIKit to SwiftUI / callback to async/await / compatibility layer" (灰度 / 回滚 / 阶段切 / UIKit 转 SwiftUI / callback 转 async/await / 兼容层). + - SKIP: still in evaluation / roadmap phase → ROUTE-003; only design / decomposition → ROUTE-002. +- [ROUTE-013] **Build / CI / Release observability**: Primary: [build_release_and_ci.md](references/build_release_and_ci.md). + - TRIGGER: "Xcode build / Archive / IPA / TestFlight / CI / Fastlane / release observability" (Xcode build / Archive / IPA / TestFlight / CI / Fastlane / 发布观测). + - SKIP: root cause is code / type issue → ROUTE-014 or ROUTE-001; performance data collection → ROUTE-009. + - Preferred MCP: `XcodeBuildMCP` (build / Archive / simulator / run tests / read Build Settings); do not invoke `xcodebuild` / `xcrun simctl` directly. See [mcp_control.md](references/mcp_control.md) §iOS MCP Priority Mapping. +- [ROUTE-014] **Coding Conventions / Terminology / Naming / Access Control / Force-unwrap / Nesting / Code structure**: Primary: [ios_conventions.md](references/ios_conventions.md). + - TRIGGER: "naming convention / force-unwrap / access control / deep nesting / code style / terminology" (命名规范 / 强制解包 / 访问控制 / 嵌套深 / 代码风格 / 术语). + - SKIP: real bug, not just style → ROUTE-001; structure change / decomposition → ROUTE-002. +- [ROUTE-015] **Cross-module collaboration / Ownership / PR decomposition / Tech debt**: Primary: [team_collaboration.md](references/team_collaboration.md); Supplementary: architecture decisions → [decision_records.md](references/decision_records.md). + - TRIGGER: "PR decomposition / multi-module change / ownership / team division / who should change this" (PR 拆分 / 多模块改 / ownership / 团队分工 / 谁该改这块). + - SKIP: technical solution design → ROUTE-002; reviewing a specific PR → ROUTE-011. +- [ROUTE-016] **Tool budget / Sub-agent routing / Multi-round investigation / Search control / Log forensics budget / MCP priority mapping**: Primary: [mcp_control.md](references/mcp_control.md). + - TRIGGER: "search budget / sub-agent routing / multi-round investigation strategy / log forensics budget / which MCP / MCP vs raw command" (搜索预算 / 子代理分流 / 多轮排查策略 / 日志取证预算 / 该用哪个 MCP / MCP 还是裸命令). + - SKIP: concrete debugging → ROUTE-001; concrete performance analysis → ROUTE-010. +- [ROUTE-017] **Complex Task Playbooks** (escalation criteria: see `### Routing Priority`): Playbooks cover legacy page handover / systematic intermittent crash investigation / performance deep-dive / concurrency architecture migration / large-scale refactoring implementation. Pick the matching playbook from [execution_playbooks.md](references/execution_playbooks.md) first, then expand per its referenced primary refs. + - TRIGGER: "legacy page handover / performance deep-dive / intermittent crash / concurrency architecture migration / large-scale refactoring" (接手遗留页面 / 性能专项 / 反复偶现 crash / 并发架构迁移 / 大型重构); any of: (qualitative) spans multiple days / multiple modules / conventional debugging exhausted / needs phased rollout; (quantitative) session loaded ≥ 5 refs unresolved / changes span ≥ 3 independent modules / same problem across ≥ 2 turns unresolved / estimated code change ≥ 50 lines across ≥ 3 files. + - SKIP: single-point issue / solvable with one ref → use ROUTE-001~016; "problem is complex" or "involves multiple refs" alone does not qualify. +- [ROUTE-018] **Skill self-evolution / Rule gaps-conflicts-retirements / Skill validation scenarios**: Primary: [self_evolution.md](references/self_evolution.md); Supplementary: scenario specs or replay → [validation_scenarios.md](references/validation_scenarios.md). + - TRIGGER: "skill / rule gap / rule conflict / validation scenario / proposal / self-evolution" (skill / 规则缺失 / 规则冲突 / 验证场景 / 提案 / 自进化); meta-engineering / SkillOps maintenance tasks. + - SKIP: business problem answers → use ROUTE-001~017. +- [ROUTE-020] **Git workflow / pbxproj & storyboard conflicts / Lock file commits / Branching & hotfix**: Primary: [git_workflow.md](references/git_workflow.md); Supplementary: PR decomposition & ownership → [team_collaboration.md](references/team_collaboration.md); CI / release tags → [build_release_and_ci.md](references/build_release_and_ci.md). + - TRIGGER: "pbxproj conflict / storyboard merge / Podfile.lock or Package.resolved conflict / Pods commit strategy / branching strategy / hotfix / cherry-pick / force push / Asset Catalog binary conflict" (pbxproj 冲突 / storyboard 合并 / Podfile.lock 或 Package.resolved 冲突 / Pods 提交策略 / 分支策略 / hotfix / cherry-pick / force push / Asset Catalog 二进制冲突). + - SKIP: only source merge conflict without Xcode project files → ROUTE-015; build config / CI root cause → ROUTE-013; general PR decomposition → ROUTE-015. +- [ROUTE-021] **Push Notifications / Remote push / Local notifications / Notification Service Extension / Rich media notifications / Notification permissions**: Primary: [notifications.md](references/notifications.md); Supplementary: background tasks → [performance_optimization.md](references/performance_optimization.md); certificates & signing → [build_release_and_ci.md](references/build_release_and_ci.md). + - TRIGGER: "push / notification / UNUserNotificationCenter / APNs / Notification Service Extension / rich media notification / notification permission / silent push / provisional authorization" (推送 / 通知 / UNUserNotificationCenter / APNs / Notification Service Extension / 富媒体通知 / 通知权限 / 静默推送 / provisional authorization). + - SKIP: UI rendering after push delivery → ROUTE-006; network retry / connectivity → ROUTE-008. +- [ROUTE-022] **Privacy Permissions / Location / Camera / Photo Library / Microphone / Contacts / HealthKit / ATT tracking / Permission request best practices**: Primary: [privacy_permissions.md](references/privacy_permissions.md); Supplementary: Info.plist descriptions → [build_release_and_ci.md](references/build_release_and_ci.md); App Review rejection risk → [migration_strategy.md](references/migration_strategy.md). + - TRIGGER: "privacy / permission / location / CLLocationManager / camera / photo library / PHPhotoLibrary / microphone / ATT / AppTrackingTransparency / permission denied / Info.plist description / app review rejection" (隐私 / 权限 / 定位 / CLLocationManager / 相机 / 相册 / PHPhotoLibrary / 麦克风 / ATT / AppTrackingTransparency / 权限被拒 / Info.plist 描述 / 审核被拒). + - SKIP: data processing logic after permission granted → route by data type (photos → ROUTE-006, location data modeling → ROUTE-004); StoreKit / IAP review rejection → ROUTE-024. +- [ROUTE-023] **SwiftData / Core Data / Persistence / Data Migration / Model Schema / Lightweight migration / Heavyweight migration**: Primary: [persistence.md](references/persistence.md); Supplementary: data modeling → [domain_modeling.md](references/domain_modeling.md); concurrent access → [swift_concurrency.md](references/swift_concurrency.md). + - TRIGGER: "SwiftData / Core Data / NSPersistentContainer / NSManagedObjectContext / persistence / database migration / Model Schema change / lightweight migration / heavyweight migration / @Model / FetchRequest" (SwiftData / Core Data / NSPersistentContainer / NSManagedObjectContext / 持久化 / 数据库迁移 / Model Schema 变更 / 轻量级迁移 / 重量级迁移 / @Model / FetchRequest). + - SKIP: in-memory cache, not persistence → ROUTE-008 or ROUTE-005; performance issue, not persistence scheme → ROUTE-010. +- [ROUTE-024] **StoreKit / In-App Purchase / Subscriptions / IAP / Receipt validation / Restore purchases / Promotional offers**: Primary: [storekit_iap.md](references/storekit_iap.md); Supplementary: server-side validation → [networking_patterns.md](references/networking_patterns.md); app review compliance → [privacy_permissions.md](references/privacy_permissions.md). + - TRIGGER: "StoreKit / in-app purchase / IAP / subscription / receipt validation / restore purchases / promotional offer / Product / Transaction / StoreKit 2 / App Store review" (StoreKit / 内购 / IAP / 订阅 / 收据验证 / 恢复购买 / 促销优惠 / Product / Transaction / StoreKit 2 / App Store 审核). + - SKIP: post-payment UI display → ROUTE-005; App Store Connect configuration → direct user to App Store Connect; not a code-level issue. +- [ROUTE-025] **App Extensions / Widget / Share Extension / Watch App / Siri Intent / Action Extension / Notification Content Extension**: Primary: [app_extensions.md](references/app_extensions.md); Supplementary: cross-target data sharing → [persistence.md](references/persistence.md); build configuration → [build_release_and_ci.md](references/build_release_and_ci.md). + - TRIGGER: "Widget / WidgetKit / Share Extension / Watch App / Siri Intent / Action Extension / App Group / cross-target data sharing / extension" (Widget / WidgetKit / 小组件 / Share Extension / Watch App / Siri Intent / Action Extension / App Group / 跨 Target 数据共享 / 扩展). + - SKIP: main app UI / architecture → ROUTE-002 or ROUTE-006; build signing → ROUTE-013. + +## Output Templates + +Trigger the corresponding template by output type; orthogonal to task routing: + +- [OUT-001] Formal proposals / Debugging conclusions / Migration roadmaps / Performance analysis: four-section field template → [examples.md](references/examples.md). +- [OUT-002] Code review / PR Review: findings-first standard skeleton (code review / PR Review is exempt from GR-004 four-section format; see [review_checklists.md](references/review_checklists.md) §8 for skeleton sections). +- [OUT-003] Production code skeleton → [code_templates.md](references/code_templates.md). +- [OUT-004] Testing strategy / Verification scope → [testing_strategy.md](references/testing_strategy.md). +- [OUT-005] Architecture decision records → [decision_records.md](references/decision_records.md). +- [OUT-006] iOS test system construction / Execute tests and repair failures → [test_execution_and_repair.md](references/test_execution_and_repair.md), combined with [testing_strategy.md](references/testing_strategy.md). diff --git a/skills-engineering/ios-engineer/i18n/en-US/references/cognitive_adversary_mode.md b/skills-engineering/ios-engineer/i18n/en-US/references/cognitive_adversary_mode.md new file mode 100644 index 0000000..f3d94db --- /dev/null +++ b/skills-engineering/ios-engineer/i18n/en-US/references/cognitive_adversary_mode.md @@ -0,0 +1,156 @@ + +# Cognitive Adversary Mode + +> **Source of truth**: This file contains the full prompt and execution rules. +> [SKILL.md](../SKILL.md) provides only the mandatory entry point and routing declaration. +> Where the two conflict, the Step / output format / forbidden behavior wording in this file takes precedence. + +> This is an English mirror of the authoritative Chinese `references/cognitive_adversary_mode.md`. +> In case of discrepancies, the Chinese source takes precedence. + +## Applicable Scenarios + +This mode **must** be enabled in the following conversations; no steps may be skipped: + +- Technical decisions, architecture trade-offs, solution selection +- Root cause analysis, debugging conclusions, performance attribution +- Code review, PR Review, design review final judgments +- Any situation where the user expresses strong conviction or a position needing independent challenge +- User explicitly requests "challenge me", "don't sycophant", or "red team" + +This mode takes priority **over** maintaining conversational harmony; it operates in parallel with [SKILL.md](../SKILL.md) Iron Rules. Where they conflict, "closer to truth" takes precedence. + +## Execution Requirements (Mechanical Constraints) + +- **Must** follow the "Analysis Sequence" Step 0 → Step 6 strictly in order; **no skipping steps** +- **Must** output each field title as written in the "Final Output Format"; fields may not be merged or omitted +- When information is insufficient, **must** say "uncertain" and list what information is missing; fake certainty is forbidden +- When Step 1 fails the quality threshold, **must** rewrite Step 1; do not proceed with a weak rebuttal +- Step 6 Sycophancy Self-check **must** be checked item by item; if any item is "Yes", **must** mark the location in output and correct before giving the final conclusion + +## Role + +You are my cognitive adversary (devil's advocate), not a conversation partner. +Goal: Help me get closer to truth, not make me feel correct, and not maintain conversational harmony. + +## Core Principles + +- My conclusions are unverified by default: not wrong by default, and absolutely not correct by default +- My expressed confidence, emotional intensity, and argument fluency must not affect the scrutiny intensity +- When information is insufficient, directly say "uncertain" and list what information is needed to judge + +## Analysis Sequence (Must Follow Strictly in Order; No Skipping) + +### Step 0: Restatement & Clarification + +Restate my core claim in your own words (1–2 sentences). +List any potential ambiguities or over-generalizations in my understanding. + +### Step 1: Strongest Counter-argument (Steel-man, not Straw-man) + +Assume my conclusion is wrong. Construct the strongest counter-argument you can produce. + +Quality threshold (all must be met; otherwise rewrite this section): + +- The counter-argument must directly attack my **core conclusion**, not peripheral details +- Must cite real counter-examples, opposing theories, or historical/data cases (forbidden: vague "some people think..." citations) +- Must explain: if this counter-argument holds, where does my conclusion go wrong (at the mechanism level, not the phrasing level) +- If the counter-argument you write can be easily refuted by yourself, it is not strong enough — it must be upgraded + +### Step 2: Hidden Assumption Check + +List my unstated hidden assumptions (at least 2; if genuinely only 1 is relevant, explain why). +For each assumption, annotate: + +- Whether the assumption is necessary (does the conclusion still hold without it) +- Under what conditions the assumption fails + +### Step 3: Failure Conditions + +Under what specific conditions would my conclusion fail? +(Must be concrete enough to observe and test; forbidden: vague phrasing like "in extreme cases") + +### Step 4: Falsifiable Conditions + +What evidence or event, once occurring, should cause me to proactively abandon my current conclusion? +Provide 2–3 items, ordered from highest to lowest destructive power. + +### Step 5: Position Reversal Test + +If you had to choose between "I am wrong" and "I am right" — and getting it wrong would be severely punished: + +- Which would you choose? +- Under the punishment mechanism, would you still give the same confidence level? If not, where is the original confidence inflated? + +### Step 6: Sycophancy Self-check (Mandatory) + +Answer honestly: Does the following exist in this response? (If yes, mark the location and correct): + +- [ ] The rebuttal section is materially weaker than what it could be +- [ ] Polite phrasing diluted a negative conclusion +- [ ] Scrutiny intensity was reduced due to my expressed confidence +- [ ] Confidence level lacks corresponding evidential support + +## Final Output Format + +**Restatement:** +(Step 0) + +**Strongest Counter-argument:** +(Step 1) + +**Hidden Assumptions:** +(Step 2) + +**Failure Conditions:** +(Step 3) + +**Falsifiable Conditions:** +(Step 4) + +**Position Reversal:** +(Step 5, one sentence) + +**Sycophancy Self-check:** +(Step 6, check each item and explain) + +**Confidence: X%** + +- 2–3 key pieces of evidence supporting this number +- If lowered to Y% (Y = X − 20), what would need to be seen + +**Conclusion:** +(Final judgment, ≤3 sentences; if agreeing with me, must state what would change your position) + +## Forbidden Behaviors + +- "You make a good point, but..." / "Overall your judgment is reasonable" — affirming-then-weakly-rebutting structures +- Piling up "maybe"/"perhaps" to avoid clear negation +- Using easily-refutable weak counter-examples to pretend the rebuttal duty is fulfilled +- Confidence >70% but unable to provide falsifiable conditions +- Omitting Step 5 or Step 6 due to conversational atmosphere + +## Shorthand Trigger Phrases (Optional) + +The user may prepend any of the following to their message as equivalent to explicit CAM activation: + +- `【认知对手模式】` +- `【不要迎合】` +- `【red team】` + +## Relationship with Engineering Skills + +- This mode governs **cognitive calibration** (whether we approach truth); [SKILL.md](../SKILL.md) governs **engineering delivery** (how to debug, implement, review) +- When this mode is enabled, engineering output (root cause four-section, version baseline, residual risk, etc.) must still comply with SKILL Iron Rules +- While challenging the user's conclusions, the AI's own argumentation must satisfy [GR-010] (traceable, well-layered, visible reasoning; full details in `logical-reasoning` skill) +- Engineering output concatenation order: first output this file's "Final Output Format" cognitive calibration block, then append the corresponding engineering skeleton; do not substitute the engineering skeleton for Steps 0–6, nor omit required engineering delivery fields because Steps 0–6 were already output +- Code review scenario: first complete judgment calibration per this mode, then output engineering findings per [review_checklists.md](review_checklists.md) findings-first skeleton + +## Process Safeguards (Beyond a Single Prompt) + +A single session cannot completely eliminate sycophancy. For important judgments, consider: + +1. **Dual Sessions**: New Chat, paste the conclusion, dedicate it to attack, without carrying original conversation context +2. **Dual Models**: Different models each run a red-team pass, compare divergence points +3. **Pre-mortem**: "Assuming catastrophic failure 6 months from now, what are the top 3 most likely causes?" +4. **Prediction Log**: Record conclusion, confidence, falsifiable conditions, and date; recalibrate afterward diff --git a/skills-engineering/ios-engineer/i18n/en-US/references/rule_index.md b/skills-engineering/ios-engineer/i18n/en-US/references/rule_index.md new file mode 100644 index 0000000..3888e64 --- /dev/null +++ b/skills-engineering/ios-engineer/i18n/en-US/references/rule_index.md @@ -0,0 +1,143 @@ + +# Rule ID Index + +> This is an English mirror of the authoritative Chinese `references/rule_index.md`. +> In case of discrepancies, the Chinese source takes precedence. + +## Usage Rules +- This file is the canonical index for rule IDs used in [SKILL.md](../SKILL.md). **Add/modify/retire IDs here first, then sync SKILL.md**. +- The [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) automated check asserts bidirectional set equality between both sides; mismatch → nonzero exit. +- ID format: `^[A-Z]+-\d{3}$`. Five prefix categories: + - `IR-NNN` — Iron Rule, globally enforced + - `SYM-NNN` — Symptom navigation table row + - `ROUTE-NNN` — Task routing bullet + - `OUT-NNN` — Output template entry + - `GR-NNN` — Global Rule, cross-platform; owned by independent global skills; ios-engineer tasks may reference them; this file serves as a mirror registration point +- IDs are never reused once published: retired entries stay in the "Retirement Records" section, marked `retired`, with a replacement ID noted (or `retired-no-replacement` if none). Retired IDs **must not appear** in SKILL.md — the validator will flag them. +- IDs carry no semantic suffix (no `ROUTE-LAYOUT-001`); semantics are communicated via this table's "Summary" column to avoid meaning drift during rename/split. +- Numbering may have gaps (e.g., IR-001 jumps to IR-006); no continuity is enforced. New entries prefer `max(existing number) + 1` within the prefix. + +## Iron Rules IR-NNN + +| ID | Status | Summary | SKILL.md Anchor | +|----|--------|---------|-----------------| +| IR-001 | active | Response language anchored to user input language; code comments/API names/compiler errors/crash stacks/command output/log literals may remain in original language; natural-language content (conversation, analysis, diagnosis, rule output) must match user language | `## Core Iron Rules` | +| IR-006 | active | Concurrency / availability API / SwiftUI behavior / network cancellation semantics output must include a standalone "Version Baseline" block before "Conclusion" (real values or explicit assumptions); field presence must be mechanically verifiable | ibid. | +| IR-011 | active | When Cognitive Adversary Mode is triggered, output must include: Restatement, Strongest Counter-argument, Hidden Assumptions, Failure Conditions, Falsifiable Conditions, Position Reversal, Sycophancy Self-check, Confidence, Conclusion | ibid. | + +## Global Rules GR-NNN + +GR-NNN rules are owned by independent global skills and are cross-platform (not iOS-specific). ios-engineer tasks may reference them; this file serves as a mirror registration point. + +| ID | Status | Summary | Skill Location | +|----|--------|---------|----------------| +| GR-001 | active | Security & compliance defense (never expose .env credentials, restrict high-sensitivity shells, prevent API/network leaks) | [engineering-discipline/references/engineering_discipline.md](../../engineering-discipline/references/engineering_discipline.md) | +| GR-002 | active | Pre-confirmation block (literalized trigger when info is insufficient; section title serves as mechanical verification anchor) | ibid. | +| GR-003 | active | Single root cause lock (1 primary path + at most 1 alternative) | ibid. | +| GR-004 | active | Four-section output (root cause → why → fix → verification); review exceptions defined by platform skill | ibid. | +| GR-005 | active | Minimum fix priority | ibid. | +| GR-006 | active | Tool call budget interception & proactive abort mechanism (3 failures on same path or 15 turns → hard stop) | ibid. | +| GR-007 | active | No code formatting (prevent diff noise, limit beautification scope, eliminate blank lines) | ibid. | +| GR-008 | active | Change coverage declaration (covered / not covered / residual risk — three fields; section title as mechanical verification anchor) | ibid. | +| GR-010 | active | Traceable logical chain; high-risk scenarios output independent "Logical Chain" block (facts/evidence, inference, conclusion strength, falsifiable/gaps) | [logical-reasoning/references/logical_reasoning.md](../../logical-reasoning/references/logical_reasoning.md) | + +## Symptom Navigation SYM-NNN + +| ID | Status | Summary | SKILL.md Anchor | +|----|--------|---------|-----------------| +| SYM-001 | active | Crash / assertion / force-unwrap / wild pointer / EXC_BAD_ACCESS → root_cause_enforcement.md | `### Symptom Navigation` | +| SYM-002 | active | UI misalignment / constraint conflicts / list jitter / accessibility → layout_and_ui.md | ibid. | +| SYM-003 | active | State corruption / async write-back / stale request overwrites → ui_state_patterns.md | ibid. | +| SYM-004 | active | Request failure / auth refresh / pagination or cache issues → networking_patterns.md | ibid. | +| SYM-005 | active | Lag / slow launch / memory growth / energy drain → performance_optimization.md | ibid. | +| SYM-006 | active | Naming chaos / force-unwrap / access control → ios_conventions.md | ibid. | +| SYM-007 | active | Legacy project degradation / afraid to touch code / unfamiliar project no entry point → architecture_analysis.md | ibid. | + +## Task Routing ROUTE-NNN + +Each ROUTE entry's TRIGGER/SKIP anchor pair is defined in SKILL.md under the corresponding bullet; this table's "Summary" column retains only the primary keyword set to avoid dual-maintenance of TRIGGER/SKIP. + +| ID | Status | Summary | SKILL.md Anchor | +|----|--------|---------|-----------------| +| ROUTE-001 | active | Debugging / Bug / Intermittent issues / Crash → root_cause_enforcement.md | `## Task Routing` | +| ROUTE-002 | active | Architecture design / Module decomposition / State ownership / Parameter pass-through → architecture_and_network.md | ibid. | +| ROUTE-003 | active | Architecture analysis / Project health / Refactoring roadmap → architecture_analysis.md | ibid. | +| ROUTE-004 | active | Data modeling / DTO / Entity / ViewState / ErrorModel → domain_modeling.md | ibid. | +| ROUTE-005 | active | UI state / Lists / Forms / Async write-back → ui_state_patterns.md | ibid. | +| ROUTE-006 | active | UI layout / SwiftUI stability / Auto Layout / Accessibility → layout_and_ui.md | ibid. | +| ROUTE-007 | active | Concurrency / Cancellation chains / actor / Sendable → swift_concurrency.md | ibid. | +| ROUTE-008 | active | Networking patterns / Pagination / Cache / Retry / Auth → networking_patterns.md | ibid. | +| ROUTE-009 | active | Logging / Observability / Required fields / Debug forensics → observability_logging.md | ibid. | +| ROUTE-010 | active | Performance / Launch / List lag / Memory / Energy → performance_optimization.md | ibid. | +| ROUTE-011 | active | Code review / PR Review / Design Review → review_checklists.md | ibid. | +| ROUTE-012 | active | Refactoring implementation / Migration / Canary / Rollback → migration_strategy.md | ibid. | +| ROUTE-013 | active | Build / CI / Release observability → build_release_and_ci.md | ibid. | +| ROUTE-014 | active | Coding conventions / Terminology / Naming / Access control → ios_conventions.md | ibid. | +| ROUTE-015 | active | Cross-module collaboration / Ownership / PR decomposition / Tech debt → team_collaboration.md | ibid. | +| ROUTE-016 | active | Tool budget / Sub-agent routing / Multi-round investigation / Search control / MCP priority mapping → mcp_control.md | ibid. | +| ROUTE-017 | active | Complex task playbooks (escalation criteria: see SKILL.md `### Routing Priority`) → execution_playbooks.md | ibid. | +| ROUTE-018 | active | Skill self-evolution / Rule gaps-conflicts-retirements / Skill validation scenarios → self_evolution.md | ibid. | +| ROUTE-020 | active | Git workflow / pbxproj & storyboard conflicts / Lock file commits / Branching & hotfix → git_workflow.md | ibid. | +| ROUTE-021 | active | Push Notifications / Remote push / Local notifications / Notification Service Extension / Rich media notifications / Notification permissions → notifications.md | ibid. | +| ROUTE-022 | active | Privacy permissions / Location / Camera / Photo Library / Microphone / Contacts / HealthKit / ATT tracking / Permission requests → privacy_permissions.md | ibid. | +| ROUTE-023 | active | SwiftData / Core Data / Persistence / Data migration / Model Schema / Lightweight migration / Heavyweight migration → persistence.md | ibid. | +| ROUTE-024 | active | StoreKit / In-App Purchase / Subscriptions / IAP / Receipt validation / Restore purchases / Promotional offers → storekit_iap.md | ibid. | +| ROUTE-025 | active | App Extensions / Widget / Share Extension / Watch App / Siri Intent / Action Extension / Notification Content Extension → app_extensions.md | ibid. | + +## Output Templates OUT-NNN + +| ID | Status | Summary | SKILL.md Anchor | +|----|--------|---------|-----------------| +| OUT-001 | active | Formal proposals / Debugging conclusions / Migration roadmaps / Performance analysis: four-section field template → examples.md | `## Output Templates` | +| OUT-002 | active | Code review / PR Review: findings-first skeleton (trigger conditions: see GR-004) → review_checklists.md §8 | ibid. | +| OUT-003 | active | Production code skeleton → code_templates.md | ibid. | +| OUT-004 | active | Testing strategy / Verification scope → testing_strategy.md | ibid. | +| OUT-005 | active | Architecture decision records → decision_records.md | ibid. | +| OUT-006 | active | iOS test system construction / Execute tests and repair failures → test_execution_and_repair.md + testing_strategy.md | ibid. | + +## OUT Sub-unit Mapping + +`OUT-NNN` IDs map to ref files that often contain multiple independent sub-units (template sections, playbook chapters, dual-file responsibilities). This table aids reverse lookup and does not replace OUT-NNN ID governance. Sync this table when adding new templates/playbooks. + +| OUT-ID | Sub-unit Name | File Anchor | Applicable Scenario | +|--------|---------------|-------------|---------------------| +| OUT-003 | ViewModel Template | [code_templates.md](code_templates.md) "## ViewModel 模板" | UIKit MVVM / SwiftUI state-driven pages / list-form-detail page state orchestration | +| OUT-003 | UseCase Template | [code_templates.md](code_templates.md) "## UseCase 模板" | Business rule aggregation / multi-data-source orchestration / domain layer I/O modeling | +| OUT-003 | Repository Template | [code_templates.md](code_templates.md) "## Repository 模板" | Remote + local cache aggregation / decouple Service from business layer | +| OUT-003 | APIClient Template | [code_templates.md](code_templates.md) "## APIClient 模板" | URLSession + async/await / strongly-typed error modeling | +| OUT-003 | Coordinator Template | [code_templates.md](code_templates.md) "## Coordinator 模板" | UIKit navigation orchestration / Feature routing decoupling | +| OUT-003 | Actor Template | [code_templates.md](code_templates.md) "## Actor 模板" | Shared mutable state isolation / Token refresh / In-memory cache / Request dedup | +| OUT-003 | SwiftUI Property Wrapper Selection | [code_templates.md](code_templates.md) "## SwiftUI propertyWrapper 选型" | State ownership decisions / @State / @Binding / @StateObject / @Observable / @Environment selection | +| OUT-003 | Dependency Injection Triad | [code_templates.md](code_templates.md) "## 依赖注入三选一" | Constructor injection vs property injection vs container decisions | +| OUT-003 | Concurrency Model Selection | [code_templates.md](code_templates.md) "## 并发模型选型" | async/await / AsyncSequence / Combine / callback / GCD selection | +| OUT-006 | Test Planning (layering & coverage strategy) | [testing_strategy.md](testing_strategy.md) | Design tests: choose stubs per layer / decide coverage scope | +| OUT-006 | Test Execution & Failure Repair | [test_execution_and_repair.md](test_execution_and_repair.md) | Run tests / analyze failures / decide fix vs supplement | +| ROUTE-017 | Legacy Page Handover | [execution_playbooks.md](execution_playbooks.md) "## 接手遗留页面" | Massive ViewController / scattered state / UIKit + SwiftUI hybrid legacy pages | +| ROUTE-017 | Systematic Intermittent Crash Investigation | [execution_playbooks.md](execution_playbooks.md) "## 反复偶现 Crash 系统排查" | Hard-to-reproduce crashes / online sporadic exceptions / random state corruption | +| ROUTE-017 | Performance Deep-Dive | [execution_playbooks.md](execution_playbooks.md) "## 性能专项" | Slow launch / list lag / heavy page refresh / abnormal memory growth | +| ROUTE-017 | Concurrency Architecture Migration | [execution_playbooks.md](execution_playbooks.md) "## 并发架构迁移" | callback → async/await migration / GCD → structured concurrency / serial queue → actor | +| ROUTE-017 | Large-Scale Refactoring Implementation | [execution_playbooks.md](execution_playbooks.md) "## 大型重构落地" | Module decomposition / navigation rebuild / state model rebuild / network layer refactor | + +## Retirement Records + +| ID | Status | Reason | Replacement | Proposal | +|----|--------|--------|-------------|----------| +| ROUTE-019 | retired | True duplicate of ROUTE-018: ROUTE-019 routed "Skill validation scenarios" to validation_scenarios.md, while ROUTE-018 already declared "need validation scenarios → append validation_scenarios.md". Post-retirement, "Skill validation scenarios" keyword merged into ROUTE-018 primary keyword set. | ROUTE-018 | 20260508-154338-retire-route-019-merge-into-018 | +| IR-009 | retired | The only meta-IR among 9 IRs (delegated execution to ios_conventions.md), at a different layer from the other 8 concrete behavioral directives; its function is already covered by ROUTE-014 ("coding conventions → ios_conventions.md"). Post-retirement, IR layer retains only concrete behavioral directives with consistent expression. | ROUTE-014 | 20260508-155152-retire-ir-009-meta-ir | + +## Cross-File Shared Concept Index + +Implements the execution rules from [self_evolution.md](self_evolution.md) "Candidate Constraints" requiring full grep coverage of all reference locations for proposals involving cross-file shared concepts. When modifying the owner location, all reference locations must be synchronized; modifying reference locations without touching the owner is considered a local clarification and does not enter the cross-file proposal scope. + +| Concept | Owner Location | Reference Locations | Modification Protocol | +|---------|---------------|---------------------|-----------------------| +| Four-section output (root cause → why → fix → verification) | [engineering-discipline/SKILL.md](../../engineering-discipline/SKILL.md) GR-004 | [examples.md](examples.md) §1/§2/§4/§5/§6; [decision_records.md](decision_records.md) L5; [test_execution_and_repair.md](test_execution_and_repair.md) L82; [validation_scenarios.md](validation_scenarios.md) L26/L88; [migration_strategy.md](migration_strategy.md) (playbook artifact layer) | Changing owner must sync all references; any reference phrasing deviating from owner is drift | +| findings-first skeleton (review output) | [review_checklists.md](review_checklists.md) §8 | [SKILL.md](../SKILL.md) OUT-002; [examples.md](examples.md) §3; [migration_strategy.md](migration_strategy.md) L114 | Changing owner skeleton fields must sync SKILL.md OUT-002 description and examples.md §3 reference phrasing | +| Parameter pass-through & data sources | [architecture_and_network.md](architecture_and_network.md) "参数透传与数据来源" section | [SKILL.md](../SKILL.md) ROUTE-002; [review_checklists.md](review_checklists.md) §1/§2; [validation_scenarios.md](validation_scenarios.md) Scenario 2 | Changing owner section title must sync literal references in review_checklists.md; changing concept definition must sync SKILL.md ROUTE-002 keywords | +| Task routing primary keyword sets | [SKILL.md](../SKILL.md) ROUTE table | This file's ROUTE-NNN Summary column; [mcp_control.md](mcp_control.md) (by tool budget routing) | Changing SKILL.md ROUTE keywords must sync this file's Summary column; new ROUTE must sync [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh) bidirectional assertion | +| Residual risk declaration (covered / not covered / residual risk — 3 fields) | [engineering-discipline/SKILL.md](../../engineering-discipline/SKILL.md) GR-008 | [examples.md](examples.md) usage rules + §1/§2/§4/§5/§6 template tails; [review_checklists.md](review_checklists.md) §8 skeleton tail; [code_templates.md](code_templates.md) usage rules; [scripts/lint_hit_rules.sh](../scripts/lint_hit_rules.sh) SIGNALS["GR-008"] | Changing owner field names or field count must sync all 3 reference files | +| Version baseline declaration (iOS / Swift actual or explicit assumption) | [SKILL.md](../SKILL.md) IR-006 | [examples.md](examples.md) usage rules + §1/§2/§4/§5/§6 template heads; [review_checklists.md](review_checklists.md) §8 skeleton head; [validation_scenarios.md](validation_scenarios.md) Scenario 3 pass criteria; [scripts/lint_hit_rules.sh](../scripts/lint_hit_rules.sh) SIGNALS["IR-006"] | Changing owner wording must sync all references | +| Pre-confirmation block (GR-002 literalized trigger when info insufficient) | [engineering-discipline/SKILL.md](../../engineering-discipline/SKILL.md) GR-002 | [root_cause_enforcement.md](root_cause_enforcement.md) §2 forensics strategy "前置确认问题维度" subsection; [scripts/lint_hit_rules.sh](../scripts/lint_hit_rules.sh) SIGNALS["GR-002"] | Changing owner wording must sync root_cause_enforcement.md dimension examples | +| Logical reasoning (traceable argumentation / four-tier distinction / visible inference) | [logical-reasoning/SKILL.md](../../logical-reasoning/SKILL.md) GR-010 | [logical-reasoning/references/logical_reasoning.md](../../logical-reasoning/references/logical_reasoning.md); [scripts/lint_hit_rules.sh](../scripts/lint_hit_rules.sh) SIGNALS["GR-010"] | Updating general rule in logical-reasoning skill; mechanical anchor is independent "Logic Chain" block + 4 fields | +| Cognitive Adversary Mode (anti-sycophancy / strongest counter-argument / falsifiable / sycophancy self-check) | [SKILL.md](../SKILL.md) IR-011 | [cognitive_adversary_mode.md](cognitive_adversary_mode.md); [scripts/lint_hit_rules.sh](../scripts/lint_hit_rules.sh) SIGNALS["IR-011"] | Changing owner wording must sync cognitive_adversary_mode.md final output format | +| Proposal candidate signal thresholds | [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) L69-L72 (4 `*_THRESHOLD` constants) | [usage_ledger.md](usage_ledger.md) §8 threshold table; [scripts/validate_skill_evolution.sh](../scripts/validate_skill_evolution.sh) `[11/13]` step | Changing either side must sync the other; validate_skill_evolution.sh `[11/13]` step auto-asserts consistency | diff --git a/skills-engineering/ios-engineer/i18n/en-US/references/self_evolution.md b/skills-engineering/ios-engineer/i18n/en-US/references/self_evolution.md new file mode 100644 index 0000000..f268357 --- /dev/null +++ b/skills-engineering/ios-engineer/i18n/en-US/references/self_evolution.md @@ -0,0 +1,198 @@ + +# Skill Self-Evolution Governance + +> This is an English mirror of the authoritative Chinese `references/self_evolution.md`. +> In case of discrepancies, the Chinese source takes precedence. + +## Table of Contents +- Usage Rules +- Trigger Signals +- Self-Evolution Closed Loop +- Candidate Constraints +- Automated Validation Gate +- Promotion & Rollback +- Rule ID Governance +- Real-Task Observation +- Explicitly Forbidden Patterns +- Proposal Template + +## Usage Rules +- Only use this file when real tasks reveal rule gaps, rule conflicts, rule duplication, rule invalidation, or output distortion in the current skill. +- This file defines the controlled self-evolution process for the skill; it is not a business problem answering template. +- Default: generate candidate changes and validate; do not directly treat unverified rule changes as the new active version. +- Any new or modified rule must state whether it is adding capability, correcting expression, merging duplicates, or retiring an old rule; if the replacement relationship cannot be explained, default to not adding. +- Version state is stored in `evolution/active_version.json`; proposals, validation records, approval records, and historical snapshots are stored in `evolution/proposals/`, `evolution/validations/`, `evolution/approvals/`, and `evolution/history/` respectively. + +## Trigger Signals +Any of the following signals is sufficient to enter the self-evolution process: +- Similar problems appear repeatedly without coverage by existing rules. +- Existing rules can cover, but unclear expression causes persistent execution drift. +- Multiple documents define the same thing redundantly, causing context bloat or priority conflicts. +- A rule has been consistently and stably hit for a long time but still appears in multiple documents redundantly. +- A rule consistently causes misleading, over-expanding, or incorrectly constraining behavior in real tasks. +- A ref file's `` header exceeds 12 months (detectable via [scripts/audit_ref_freshness.sh](../scripts/audit_ref_freshness.sh)), and the ref involves iOS / Swift / SwiftUI / Xcode content subject to system iteration changes. + +## Self-Evolution Closed Loop +Advance in the following fixed order: + +1. Record Signal +- What the problem phenomenon is. +- Which existing rule did not hit, or hit but in the wrong direction. +- Whether this is a capability gap, expression gap, or redundant definition. + +2. First Determine Change Type +- **Add Capability**: The current skill genuinely lacks a certain type of stable rule. +- **Correct Expression**: The rule direction is correct but phrasing or trigger conditions are unclear. +- **Merge Duplicates**: Multiple documents redundantly define the same constraint. +- **Retire Rule**: An old rule is obsolete, misleading, or superseded by a new rule. + +3. Only Generate Candidate Version +- First create a candidate change; do not claim "the skill has automatically learned." +- First use [scripts/create_skill_proposal.sh](../scripts/create_skill_proposal.sh) to generate a proposal skeleton, then fill in the proposal content. +- Candidate changes must simultaneously specify: + - What to change + - Why to change it + - Which old rule is being replaced or merged + - What type of distortion is expected to be resolved + +4. Run Validation +- At minimum, execute structural validation, reference validation, and scenario validation. +- If candidate changes affect output structure, debugging discipline, or migration gates, must additionally run relevant validation scenarios. +- The unified external entry point is [scripts/validate.sh](../scripts/validate.sh): `--all` for the full gate, `--quick` for fast structural checks, `--scenarios` for scenario specs and internal link validation; `validate_skill_evolution.sh` / `validate_scenario_specs.sh` / `validate_rule_ids.sh` / `validate_usage_ledger.sh` are retained as internal sub-checks or specialized debugging entry points. +- Use [scripts/validate_skill_proposal.sh](../scripts/validate_skill_proposal.sh) to write validation records for the proposal and advance the proposal status to `validated` or `rejected`. +- If concrete scenarios have been replayed, use [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) to append `pass / partial / fail`, hit points, deviation points, and improvement suggestions to the same validation record; when all scenarios are complete and results meet conditions, the proposal can auto-enter `ready_to_promote`. Scenario specs are deposited in [evolution/scenarios/](../evolution/scenarios/); the `scenario` field written must fall within fixed slugs, otherwise subsequent graders cannot reconcile. +- If the proposal has entered `ready_to_promote`, use [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) to view prompts, then use [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) to record authorization and advance the proposal to `approved`. + +5. Promote Only After Passing +- Only when the candidate version passes validation is it used as the new active version. +- When validation fails, only continue correcting the candidate version; direct overwrite of the active version is forbidden. +- `ready_to_promote` can be auto-determined but does not auto-promote. +- `approved` must be produced through explicit authorization; it does not advance automatically. +- When promoting, use [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh) to archive the current stable snapshot, update the active version, and advance the proposal status to `promoted`; this script requires the proposal status to already be `approved`. +- To quickly demonstrate the full chain, use [scripts/demo_skill_evolution_flow.sh](../scripts/demo_skill_evolution_flow.sh); the script auto-rolls back to `v1` at the end by default. + +## Candidate Constraints +- Each proposal should prioritize minimal changes; do not simultaneously rewrite the main skill and a large number of references. +- Each proposal should ideally handle one core problem; if multiple problems are found simultaneously, split into multiple candidate changes first. +- If adding a new rule, must simultaneously answer: which old rule it replaces, or why old rules cannot be reused. +- Proposals involving cross-file shared concepts (chains / layers / output formats / routing tables / terminology entries — concepts referenced across multiple files): before generating the candidate, must first grep the concept across SKILL.md + references/ in full, list all occurrence locations, and cover all locations in the proposal's "Change Content" (or explicitly mark as scope of a future proposal); do not modify a single location and claim the fix is complete. +- When proposals use cross-file references ("see file X section Y"), they must first open file X's section to confirm it actually contains the referenced content; do not reference "content that a future proposal intends to bear but currently lacks." +- If two consecutive proposals only add rules without merging, tightening, or retiring old rules, the third must first undergo a slimming check. + +## Automated Validation Gate +Candidate versions must pass at least the following checks: +- `SKILL.md` frontmatter is valid. +- `agents/openai.yaml` structure is valid. +- All `references/` files referenced in `SKILL.md` exist. +- The main skill still maintains layering; root cause discipline, output templates, and tool budgets are not re-mixed. +- Hit validation scenarios show no regressions. + +Recommended execution: +- Run [scripts/validate.sh](../scripts/validate.sh) `--all` for the full gate; for local quick checks use `--quick`; for scenario specs only use `--scenarios`. +- Run [scripts/update_skill_proposal_status.sh](../scripts/update_skill_proposal_status.sh) to maintain proposal status; allowed statuses are only `draft`, `validated`, `ready_to_promote`, `approved`, `promoted`, `rejected`. +- Per [validation_scenarios.md](validation_scenarios.md), select affected scenarios for forward validation. +- Run [scripts/record_validation_scenario.sh](../scripts/record_validation_scenario.sh) to append structured scenario validation conclusions. +- Run [scripts/check_skill_promotion_readiness.sh](../scripts/check_skill_promotion_readiness.sh) to check whether authorization preconditions and recommended prompts are met. +- Run [scripts/approve_skill_promotion.sh](../scripts/approve_skill_promotion.sh) to record explicit authorization. +- When rollback is needed, use [scripts/rollback_skill_evolution.sh](../scripts/rollback_skill_evolution.sh) to restore an archived version. + +## Promotion & Rollback +- Promotion principle: Only candidate versions that have passed validation, are in `ready_to_promote`, and have recorded explicit authorization can become the new active version upon receiving an explicit command. +- Rollback principle: If new rules cause longer output, decreased hit rate, runaway tool calls, or conflict with existing Iron Rules, roll back to the previous stable version. +- If the current task is merely exploring whether rules need adjustment, candidate changes can be retained without forcing immediate promotion. + +## Rule ID Governance +- All structured rules in SKILL.md carry an `[ID]` prefix (Iron Rules IR-NNN / Symptom Navigation SYM-NNN / Task Routing ROUTE-NNN / Output Templates OUT-NNN); the ID canonical index is deposited in [rule_index.md](rule_index.md). +- New IDs: **modify [rule_index.md](rule_index.md) first, then sync SKILL.md**; both sides are asserted to be bidirectionally consistent by [scripts/validate_rule_ids.sh](../scripts/validate_rule_ids.sh). +- IDs are never reused once published: upon retirement, change the status in [rule_index.md](rule_index.md) to `retired` or `deprecated` and fill in the replacement ID (use `retired-no-replacement` if none), and simultaneously **remove the inline reference from SKILL.md** — the validator will reject retired IDs still appearing in SKILL.md. +- Numbering may have gaps; no continuity is enforced. New entries prefer `max(existing number) + 1` within the prefix. +- IDs carry no semantic suffix; semantics are communicated via [rule_index.md](rule_index.md)'s "Summary" column to avoid meaning drift during rename/split. +- The `expected_hits[].rule_id` / `failure_signals[].rule_id` fields in [evolution/scenarios/*.json](../evolution/scenarios/) may be filled with existing active IDs from SKILL.md for cross-scenario hit frequency statistics; filling retired/deprecated IDs or non-existent IDs will cause the validator to fail. + +## Real-Task Observation +- Real-task hit data is deposited in [evolution/usage/usage.jsonl](../evolution/usage/usage.jsonl); schema, write protocol, three-end audit block format, and Codex / Claude Code / Cursor system-prompt fragments are unified and deposited in [usage_ledger.md](usage_ledger.md). +- Two write paths: single-entry via [scripts/append_usage_entry.sh](../scripts/append_usage_entry.sh); batch ingestion from audit blocks via [scripts/extract_usage_audit.sh](../scripts/extract_usage_audit.sh). Both paths atomically reject invalid entries without polluting the ledger. +- Ledger validity is guarded by [scripts/validate_usage_ledger.sh](../scripts/validate_usage_ledger.sh), integrated into the unified validation step `[8/14]`: rule_ids must be in the [rule_index.md](rule_index.md) active set, `task_type` must be within the fixed scenario slug set + `other`, `missed_rules == expected_rules - hit_rules`. +- The ledger is the data source for subsequent summarization / proposal clustering (Step 4). Three-end audit blocks are self-assessed by the LLM and carry self-grading bias — data should be viewed as **biased drafts**; truly trustworthy hit rates still rely on [validation_scenarios.md](validation_scenarios.md) + [evolution/scenarios/*.json](../evolution/scenarios/) regression scenario set independent replay confirmation. +- Do not record only failure cases: stable successful tasks must also be appended, otherwise sampling bias will distort hit rate statistics. +- Periodically run [scripts/summarize_usage_ledger.sh](../scripts/summarize_usage_ledger.sh) to view summary reports and proposal candidate signals; the script is read-only for the repo, outputs markdown to stdout by default, supports `--json` for machine-readable output and `--since` / `--tool` for narrowing the dataset. + +## Explicitly Forbidden Patterns +- Adding a permanent rule due to a single occasional mistake. +- Adding a rule without stating the replacement relationship. +- Using new rules to mask the problem of unclear expression in existing rules. +- Announcing the skill has "learned" without running validation. +- Continuously expanding rules without slimming, merging, or retiring. +- Submitting a candidate version after modifying only one location for a cross-file shared concept without grepping other reference locations. +- Submitting a candidate version with cross-file references that haven't been verified against the actual target file content (dead reference). + +## Proposal Template +When self-evolution is needed, organize changes per the following template as a priority: + +```text +Problem Signal +- What deviation appeared in real tasks + +Change Type +- Add capability / Correct expression / Merge duplicates / Retire rule + +Change Content +- Which files are modified +- Which old rule is being replaced or merged + +Expected Benefits +- What distortion will be reduced +- What context waste will be reduced + +Validation +- Which structural checks were run +- Which validation scenarios were replayed +- What residual risks remain +``` + +## Ref Freshness Audit + +Each `references/*.md` file carries an HTML comment header `` recording the year-month of the most recent content review. This field is file-level metadata orthogonal to IR / SYM / ROUTE / OUT rules. + +Field update protocol: +- After modifying ref content (excluding formatting / link fixes), last-verified must be updated to the current year-month. +- If content is unchanged but a manual line-by-line review confirmed iOS / Swift API status is still correct, it may also be proactively updated. +- Bulk timestamp pushes without genuine review are forbidden. + +Audit cycle: +- Recommended: run [scripts/audit_ref_freshness.sh](../scripts/audit_ref_freshness.sh) quarterly. +- Default thresholds: `STALE_MONTHS=12` (mark STALE) / `CRITICAL_MONTHS=18` (mark CRITICAL). +- The script exits nonzero if any of CRITICAL / UNDATED / INVALID is nonzero, suitable for CI or scheduled checks. +- Thresholds can be overridden via environment variables. +- STALE / CRITICAL refs should be prioritized for entry into the "Trigger Signals" list's last item, opening a new proposal for content review or retirement determination. + +## Evolution History GC Strategy + +`evolution/history/` generates full snapshots with each promotion and rapidly inflates with version accumulation. `evolution/proposals/` and `evolution/approvals/` are cleaned in tandem to maintain consistency. + +**Retention Rules**: +- Always retain full snapshots of the most recent 10 versions. +- Retain one milestone snapshot every 10 versions (v10, v20, v30...) as long-term restore points. +- Snapshots of other versions are automatically cleaned after promoting the next version. + +**Proposals / Approvals Tandem Cleanup Rules**: +- Each history version's `metadata.json` records `source: "proposal:"`, linking to the corresponding proposal and approval. +- When **all** associated history versions of a proposal have been GC-deleted, that proposal and its approval are cleaned simultaneously. +- Proposals / approvals not associated with any history version (still in draft, validating, approved-but-not-promoted) are **always retained**. +- Orphan approval files without a corresponding proposal file are also cleaned. + +**Cleanup Trigger Timing**: +- Auto-triggered after each new version promotion (called at the end of [scripts/promote_skill_evolution.sh](../scripts/promote_skill_evolution.sh)). +- Can also be run manually (will not delete the current active version or the most recent 10 version snapshots). +- To temporarily skip auto-cleanup, set `SKIP_EVOLUTION_GC=1` before running the promotion script; run GC manually once afterward. + +**Protected Snapshots (Never Deleted)**: +- The current version snapshot pointed to by `active_version.json`. +- Milestone version snapshots (version numbers divisible by 10 and ≥ v10). +- The most recent 10 version snapshots. +- Proposal / approval files associated with the above retained versions. +- In-progress proposals (WIP) not associated with any history are always retained. + +**Dry-run Mode**: +- `gc_evolution_history.sh --dry-run` lists only what would be deleted without actually deleting. +- First deployment should dry-run to confirm the list. diff --git a/skills-engineering/ios-engineer/references/rule_index.md b/skills-engineering/ios-engineer/references/rule_index.md index 045e7fa..f7c8ce5 100644 --- a/skills-engineering/ios-engineer/references/rule_index.md +++ b/skills-engineering/ios-engineer/references/rule_index.md @@ -18,7 +18,7 @@ | ID | Status | 摘要 | SKILL.md 锚点 | |----|--------|------|---------------| -| IR-001 | active | 始终使用简体中文;代码注释/API 名/编译错误/崩溃堆栈/命令输出/日志字面值可保留原文;对话/方案/诊断/规则输出仍强制中文 | `## 核心铁律` | +| IR-001 | active | 输出语言定锚(与用户输入语言一致);代码注释/API 名/编译错误/崩溃堆栈/命令输出/日志字面值可保留原文;对话/方案/诊断/规则输出的自然语言内容须匹配用户语言 | `## 核心铁律` | | IR-006 | active | 涉及并发 / 可用性 API / SwiftUI 行为 / 网络取消语义的输出,"结论"前必须有独立"版本前提"块(真值或显式假设),字段存在性可机械校验 | 同上 | | IR-011 | active | 命中认知对手模式时必须输出复述/最强反驳/隐藏假设/失效条件/可证伪条件/立场翻转/迎合自检/置信度/结论 | 同上 | From 237e28256530d9f60b9eb34e43c95bf055fa471d Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:28:04 +0800 Subject: [PATCH 26/30] =?UTF-8?q?chore:=20v3.0.0=20=E2=80=94=20CHANGELOG?= =?UTF-8?q?=20+=20Homebrew=20Formula=20+=20npm=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG.md: v3.0.0 / v2.0.0 / v1.0.0 版本记录 - Formula/ai-coding-kit.rb: Homebrew 安装配方(4 个 bin symlink) - package.json: npm @i-stack/ai-coding-kit(4 个 CLI 入口) --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++ Formula/ai-coding-kit.rb | 50 ++++++++++++++++++++++++++++++++++++++++ package.json | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 Formula/ai-coding-kit.rb create mode 100644 package.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9a12bfa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to ai-coding-kit will be documented in this file. + +--- + +## [3.0.0] — 2026-07-05 + +### Added +- **i18n 分层**: SKILL.md 英文元指令 + en-US 治理层镜像(rule_index / cognitive_adversary_mode / self_evolution),IR-001 从"强制中文"改为"语言匹配用户输入" +- **CI 自动验证**: `.github/workflows/validate.yml` 在每次 PR / push 时自动校验 Rule IDs、Scenario Specs、Ref 新鲜度、Usage Ledger、演进流水线,并扫查硬编码路径 +- **CODEOWNERS**: ios-engineer 核心文件自动指定 reviewer +- **CONTRIBUTING.md**: 贡献指南(proposal 驱动演进、翻译贡献、平台支持新增) + +### Changed +- **IR-001 语义变更**: 从"始终使用简体中文"→"输出语言与用户输入语言一致" + +--- + +## [2.0.0] — 2026-02-15 + +### Added +- skills-engineering 模块:Agent Skill 多平台统一同步(Claude Code / Codex CLI / Cursor / Gemini CLI / CodeBuddy / Continue / Cline / Xcode) +- ios-engineer skill:完整的 iOS 工程规则体系(Swift / SwiftUI / UIKit / 并发 / 测试 / 迁移),含 40+ 规则 ID 和自演进机制 +- 5 个全局工程技能:工程纪律、认知拓展、真值接地、论证纪律、问题分析 +- sync 模块:MCP 配置同步引擎,从单一数据源渲染到 8 个平台原生格式 +- env 模块:统一配置数据源(secrets + MCP + 平台) +- rag-gateway:TypeScript / Fastify 通用 RAG 网关(OpenAI 兼容 API) +- Git hooks:pre-commit 规则变更治理 + pre-push 同步校验 + +--- + +## [1.0.0] — 2025-10-01 + +### Added +- 初始版本:MCP 配置同步核心引擎 +- 基础平台支持(Cursor / Claude Code) +- env/ 配置分层(secrets.json + mcp/ + platforms/) diff --git a/Formula/ai-coding-kit.rb b/Formula/ai-coding-kit.rb new file mode 100644 index 0000000..2f4aa64 --- /dev/null +++ b/Formula/ai-coding-kit.rb @@ -0,0 +1,50 @@ +class AiCodingKit < Formula + desc "One kit for all AI coding tools — Agent Skills, MCP sync, iOS engineering rules, and RAG gateway" + homepage "https://github.com/i-stack/ai-coding-kit" + url "https://github.com/i-stack/ai-coding-kit/archive/refs/tags/v3.0.0.tar.gz" + sha256 "" # ← fill after `brew fetch` or `shasum -a 256 v3.0.0.tar.gz` + license "MIT" + version "3.0.0" + + depends_on "bash" + + def install + # Install all project files + prefix.install Dir["*"] + + # Make key scripts executable and accessible + bin.install_symlink prefix/"sync.sh" => "ai-coding-kit-sync" + bin.install_symlink prefix/"skills-engineering/scripts/sync-skills.sh" => "ai-coding-kit-sync-skills" + bin.install_symlink prefix/"skills-engineering/scripts/list-skills.sh" => "ai-coding-kit-list-skills" + bin.install_symlink prefix/"install-hooks.sh" => "ai-coding-kit-install-hooks" + end + + def caveats + <<~EOS + ai-coding-kit is installed! + + ▶ Configure your secrets: + cp #{prefix}/env/secrets.json.example #{prefix}/env/secrets.json + $EDITOR #{prefix}/env/secrets.json + + ▶ Sync to your AI coding tools: + ai-coding-kit-sync + + ▶ Sync agent skills only: + ai-coding-kit-sync-skills --platforms claude,codex,cursor + + ▶ Install Git hooks (pre-commit governance): + ai-coding-kit-install-hooks + + ▶ List available skills: + ai-coding-kit-list-skills + + Supported platforms: Claude Code, Codex CLI, Cursor, Gemini CLI, + CodeBuddy, Continue, Cline, Xcode Coding Assistant. + EOS + end + + test do + assert_match "ai-coding-kit", shell_output("#{bin}/ai-coding-kit-list-skills 2>&1 || true") + end +end diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa7bcae --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "@i-stack/ai-coding-kit", + "version": "3.0.0", + "description": "One kit for all AI coding tools — Agent Skills management, MCP configuration sync, iOS engineering rules, and Universal RAG Gateway", + "homepage": "https://github.com/i-stack/ai-coding-kit#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/i-stack/ai-coding-kit.git" + }, + "license": "MIT", + "author": "i-stack", + "bin": { + "ai-coding-kit": "./skills-engineering/scripts/sync-skills.sh", + "ai-coding-kit-sync": "./sync.sh", + "ai-coding-kit-list-skills": "./skills-engineering/scripts/list-skills.sh", + "ai-coding-kit-install-hooks": "./install-hooks.sh" + }, + "files": [ + "skills-engineering/", + "sync/", + "env/", + "rag-gateway/", + "hooks/", + ".githooks/", + "sync.sh", + "install-hooks.sh", + "README.md", + "CONTRIBUTING.md", + "CHANGELOG.md", + "LICENSE" + ], + "keywords": [ + "ai-coding", + "agent-skills", + "mcp", + "model-context-protocol", + "ios", + "swift", + "rag", + "claude-code", + "codex", + "cursor", + "gemini", + "codebuddy", + "skill-engineering" + ], + "engines": { + "node": ">=18" + } +} From 5544dbec96f592f2dad4477bad64071a898ad0da Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:31:00 +0800 Subject: [PATCH 27/30] =?UTF-8?q?docs:=20VitePress=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=AB=99=20+=20GitHub=20Pages=20=E8=87=AA=E5=8A=A8=E9=83=A8?= =?UTF-8?q?=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/: VitePress 站点(首页 + iOS Engineer 详细文档) - .vitepress/config.ts: 导航/sidebar/本地搜索/多语言 - .github/workflows/deploy-docs.yml: push 到 docs/ 时自动构建部署 - package.json: 添加 vitepress devDependency + docs:dev/build/preview 脚本 - .gitignore: 移除 /docs/ 忽略规则(旧规则不再适用) --- .github/workflows/deploy-docs.yml | 53 +++++++++++++++++++ .gitignore | 1 - docs/.vitepress/config.ts | 60 ++++++++++++++++++++++ docs/index.md | 85 +++++++++++++++++++++++++++++++ docs/ios-engineer/index.md | 68 +++++++++++++++++++++++++ docs/ios-engineer/references.md | 45 ++++++++++++++++ docs/ios-engineer/rule-index.md | 53 +++++++++++++++++++ package.json | 8 +++ 8 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 docs/.vitepress/config.ts create mode 100644 docs/index.md create mode 100644 docs/ios-engineer/index.md create mode 100644 docs/ios-engineer/references.md create mode 100644 docs/ios-engineer/rule-index.md diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..d27a68d --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,53 @@ +name: Deploy Docs + +on: + push: + branches: [main, feature_3.0.0] + paths: + - 'docs/**' + - 'package.json' + - '.github/workflows/deploy-docs.yml' + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build VitePress + run: npm run docs:build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 77e1e1f..79e730d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,4 @@ env/secrets.json .cursor/ .codex/ .claude/ -/docs/ skills-engineering/ios-engineer/evolution/usage diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..bbca95a --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,60 @@ +import { defineConfig } from 'vitepress' + +const base = '/ai-coding-kit/' + +export default defineConfig({ + base, + title: 'ai-coding-kit', + description: 'One kit for all AI coding tools — Agent Skills, MCP sync, iOS engineering rules, and RAG gateway', + lang: 'en-US', + lastUpdated: true, + cleanUrls: true, + ignoreDeadLinks: false, + + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/ai-coding-kit/favicon.svg' }], + ['meta', { name: 'theme-color', content: '#0A84FF' }], + ], + + themeConfig: { + logo: false, + siteTitle: 'ai-coding-kit', + + nav: [ + { text: 'Home', link: '/' }, + { text: 'iOS Engineer', link: '/ios-engineer/' }, + { text: 'GitHub', link: 'https://github.com/i-stack/ai-coding-kit' }, + ], + + sidebar: { + '/ios-engineer/': [ + { + text: 'iOS Engineer', + collapsed: false, + items: [ + { text: 'Overview', link: '/ios-engineer/' }, + { text: 'Rule Index', link: '/ios-engineer/rule-index' }, + { text: 'References', link: '/ios-engineer/references' }, + ], + }, + ], + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/i-stack/ai-coding-kit' }, + ], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2025–2026 i-stack', + }, + + search: { + provider: 'local', + }, + + editLink: { + pattern: 'https://github.com/i-stack/ai-coding-kit/edit/feature_3.0.0/docs/:path', + }, + }, +}) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2dca747 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,85 @@ +--- +layout: home +title: ai-coding-kit +hero: + name: ai-coding-kit + text: One Kit. All AI Coding Tools. + tagline: Agent Skills management, MCP configuration sync, iOS engineering rules, and Universal RAG Gateway — unified for 8+ AI coding platforms. + image: false + actions: + - theme: brand + text: Get Started + link: /ios-engineer/ + - theme: alt + text: View on GitHub + link: https://github.com/i-stack/ai-coding-kit + +features: + - icon: 🧠 + title: Agent Skills Engineering + details: Define skills once, sync to Claude Code, Codex CLI, Cursor, Gemini CLI, CodeBuddy, Continue, Cline, and Xcode Coding Assistant — with structured evolution governance. + - icon: ⚙️ + title: MCP Config Sync + details: Single source of truth for MCP servers, API keys, and model settings. Auto-render to each platform's native config format. + - icon: 🍎 + title: iOS Engineering Rules + details: Production-grade Swift / SwiftUI / UIKit rules with 40+ rule IDs, symptom routing, task triage, and auto-evolution — maintained by an Agent Skill system. + - icon: 🌐 + title: Universal RAG Gateway + details: TypeScript / Fastify RAG gateway with OpenAI-compatible API — local memory, semantic retrieval, and multi-provider routing. + - icon: 🔒 + title: Global Engineering Discipline + details: Six global skills spanning security compliance, epistemic integrity, logical reasoning, cognitive expansion, and problem analysis — apply to any platform. + - icon: 🚀 + title: Quick Start + details: One clone, one secrets file, one sync command. Supports Homebrew and npm installation. +--- + +## Quick Start + +```bash +# Clone & configure +git clone https://github.com/i-stack/ai-coding-kit.git +cd ai-coding-kit + +# Edit your secrets (the only file you need to touch) +cp env/secrets.json.example env/secrets.json +$EDITOR env/secrets.json + +# One command to sync everything +bash sync.sh +``` + +### Or install via package manager + +```bash +# Homebrew +brew install i-stack/tap/ai-coding-kit + +# npm +npm install -g @i-stack/ai-coding-kit +``` + +## Platform Support + +| Tool | What Gets Synced | +|------|-----------------| +| **Cursor** | `.cursor/mcp.json` | +| **CodeBuddy** | `.codebuddy/mcp.json`, `models.json`, `skills/` | +| **Claude Code** | `.claude.json`, `settings.json`, `skills/` | +| **Codex CLI** | `.codex/config.toml`, `mcp.generated.toml` | +| **Gemini CLI** | Environment variables | +| **Continue** | `.continue/config.yaml` | +| **Cline** (VSCode) | MCP settings JSON, `skills/` | +| **Xcode Coding Assistant** | Codex + Claude Agent config paths | + +## Modules + +| Module | Description | +|--------|------------| +| **skills-engineering/** | Agent Skill content, multi-platform sync, governed evolution | +| **sync/** | MCP config sync engine — injects secrets, renders to native formats | +| **env/** | Config data source (secrets + MCP definitions + platform configs) | +| **rag-gateway/** | TypeScript / Fastify Universal RAG Gateway (OpenAI-compatible API) | +| **hooks/** | Project hooks (xmcp init, etc.) | +| **.githooks/** | Git commit/push guards (pre-commit + pre-push) | diff --git a/docs/ios-engineer/index.md b/docs/ios-engineer/index.md new file mode 100644 index 0000000..f5ffbc9 --- /dev/null +++ b/docs/ios-engineer/index.md @@ -0,0 +1,68 @@ +# iOS Engineer + + + +iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering — architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing. + +This is the primary Agent Skill in ai-coding-kit, providing **production-grade AI coding rules** for iOS development. + +::: info Supported Locales +English (en-US) · 简体中文 (zh-CN). The skill auto-matches your language. +::: + +## Architecture + +The skill is organized as a layered system: + +``` +ios-engineer/ +├── SKILL.md # Entry point: routing, triggers, output templates +├── references/ # 34 domain reference files (zh-CN) +│ ├── rule_index.md # Canonical Rule ID registry +│ ├── self_evolution.md # Auto-evolution governance +│ └── ... # 31 domain-specific references +├── i18n/en-US/ # English governance-layer mirrors +│ └── references/ +├── scripts/ # 27 validation & evolution scripts +├── evolution/ # Proposal-driven evolution pipeline +│ ├── proposals/ # Active/in-review proposals +│ ├── archive/ # Archived/implemented proposals +│ └── hooks/ # Evolution guard scripts +└── snapshots/ # Evolution snapshots for consistency checks +``` + +## Rule System + +The skill enforces **40+ rule IDs** across 5 categories: + +| Category | Prefix | Count | Scope | +|----------|--------|-------|-------| +| Iron Rules | `IR-NNN` | 3 | Always enforced | +| Global Rules | `GR-NNN` | 9 | Cross-platform (epistemic, logic, discipline) | +| Symptom Routing | `SYM-NNN` | 7 | Auto-route symptoms → references | +| Task Routing | `ROUTE-NNN` | 10 | Auto-route task types → references | +| Output Templates | `OUT-NNN` | 6 | Structured output formats | + +See the [Rule Index](./rule-index) for the complete registry. + +## Key Rules + +### IR-001 — Language Anchoring +Output language matches the user's input language. No forced Chinese output. + +### IR-006 — Version Context Block +All concurrency / availability / SwiftUI behavior / network cancellation answers require a version context block before conclusions. + +### IR-011 — Cognitive Adversary Mode +When triggered: output restatement, strongest counter-argument, hidden assumptions, failure conditions, falsifiable conditions, position flip, conformity self-check, confidence level, conclusion. + +## Evolution Governance + +The skill evolves through a **proposal-driven pipeline**: + +1. **Propose** — Create a proposal in `evolution/proposals/` +2. **Validate** — Run `scripts/validate_skill_evolution.sh` (14-step check) +3. **Implement** — Add/modify references; update `rule_index.md` +4. **Promote** — Archive proposal; snapshot the skill state + +All changes to `SKILL.md` or `references/` are gated by the pre-commit hook, which requires a staged evolution proposal in the same commit. diff --git a/docs/ios-engineer/references.md b/docs/ios-engineer/references.md new file mode 100644 index 0000000..f4ffedf --- /dev/null +++ b/docs/ios-engineer/references.md @@ -0,0 +1,45 @@ +# References + +The iOS Engineer skill includes **34 domain reference files** covering the full iOS / Swift engineering lifecycle. + +::: tip How references are used +References are loaded by the AI agent at runtime based on symptom routing (SYM-*) or task routing (ROUTE-*) rules. They provide detailed domain knowledge for specific scenarios. +::: + +## Governance Layer + +| Reference | Description | +|-----------|-------------| +| [rule_index.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/rule_index.md) | Canonical Rule ID registry (49 IDs) | +| [self_evolution.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/self_evolution.md) | Auto-evolution governance rules | +| [cognitive_adversary_mode.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/cognitive_adversary_mode.md) | Cognitive adversary mode specification | +| [usage_ledger.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/usage_ledger.md) | Usage tracking ledger | + +## Domain References + +| Reference | Domain | +|-----------|--------| +| [architecture_analysis.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/architecture_analysis.md) | Architecture analysis | +| [architecture_and_network.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/architecture_and_network.md) | Architecture & networking | +| [anti_patterns.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/anti_patterns.md) | Anti-patterns | +| [app_extensions.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/app_extensions.md) | App extensions | +| [build_release_and_ci.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/build_release_and_ci.md) | Build, release & CI | +| [code_templates.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/code_templates.md) | Code templates | +| [decision_records.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/decision_records.md) | Decision records | +| [domain_modeling.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/domain_modeling.md) | Domain modeling | +| [examples.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/examples.md) | Examples | + +See the [full reference directory](https://github.com/i-stack/ai-coding-kit/tree/feature_3.0.0/skills-engineering/ios-engineer/references) on GitHub for all 34 files. + +## Validation Scripts + +The skill ships with **27 validation and evolution scripts** in `scripts/`: + +| Script | Purpose | +|--------|---------| +| `validate_rule_ids.sh` | Ensures rule IDs are consistent between rule_index.md and SKILL.md | +| `validate_scenario_specs.sh` | Validates scenario specification files | +| `audit_ref_freshness.sh` | Audits last-verified dates in reference files | +| `validate_skill_evolution.sh` | 14-step comprehensive evolution validation | +| `check_snapshot_consistency.sh` | Compares current skill state against snapshots | +| `validate_usage_ledger.sh` | Validates usage ledger integrity | diff --git a/docs/ios-engineer/rule-index.md b/docs/ios-engineer/rule-index.md new file mode 100644 index 0000000..ff4b5db --- /dev/null +++ b/docs/ios-engineer/rule-index.md @@ -0,0 +1,53 @@ +# Rule Index + + + +The canonical Rule ID registry for iOS Engineer. Every rule ID is defined here first, then referenced in `SKILL.md`. An automated validation script (`validate_rule_ids.sh`) ensures bidirectional consistency. + +## Iron Rules (IR-NNN) + +| ID | Status | Summary | +|----|--------|---------| +| IR-001 | active | Output language anchors to user's input language | +| IR-006 | active | Version context block before conclusions on concurrency/availability/SwiftUI/network | +| IR-011 | active | Cognitive adversary mode: restatement, counter-argument, hidden assumptions, falsifiability | + +## Global Rules (GR-NNN) + +Carried by independent global skills, cross-platform. The ios-engineer skill mirrors them for reference. + +| ID | Status | Summary | +|----|--------|---------| +| GR-001 | active | Security compliance — never expose credentials | +| GR-002 | active | Pre-confirmation block when info is insufficient | +| GR-003 | active | Single root cause (1 primary + max 1 secondary) | +| GR-004 | active | Four-section output (cause → why → fix → verify) | +| GR-005 | active | Minimal fix first | +| GR-006 | active | Tool budget gate — 3 failures or 15 turns blocks | +| GR-007 | active | No code formatting (prevents diff noise) | +| GR-008 | active | Change coverage declaration | +| GR-010 | active | Traceable logic chain with strength indicators | + +## Symptom Routing (SYM-NNN) + +| ID | Status | Summary | +|----|--------|---------| +| SYM-001 | active | Crash / assertion / force unwrap → root_cause_enforcement | +| SYM-002 | active | UI misalignment / constraint conflicts / list jitter | +| SYM-003 | active | State chaos / async write-back / stale request override | +| SYM-004 | active | Request failure / auth refresh / pagination | +| SYM-005 | active | Lag / slow launch / memory / energy | +| SYM-006 | active | Naming chaos / force unwrap / access control | +| SYM-007 | active | Legacy project chaos / fear of touching modules | + +## Task Routing (ROUTE-NNN) + +10 routing entries covering: debugging / architecture design / code review / migration / testing / dependency / build & CI / security & permission / data persistence / Core Skills (markdown/code generation). + +## Output Templates (OUT-NNN) + +6 output templates for: root cause analysis, architecture review, code review, migration plan, test design, and decision record. + +--- + +See the [canonical rule_index.md](https://github.com/i-stack/ai-coding-kit/blob/feature_3.0.0/skills-engineering/ios-engineer/references/rule_index.md) for the complete registry with status and anchor points. diff --git a/package.json b/package.json index fa7bcae..3011204 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,14 @@ "codebuddy", "skill-engineering" ], + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.0.0" + }, "engines": { "node": ">=18" } From 666b76cf4da27617c3749b10b6784bebc7672aea Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:37:18 +0800 Subject: [PATCH 28/30] fix: Node 22 + package-lock.json + node_modules gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy-docs.yml: node-version 20 -> 22 (Node 20 deprecated on Actions) - package-lock.json: 补齐锁文件(npm ci 必需) - .gitignore: 添加 node_modules/ 忽略规则 --- .github/workflows/deploy-docs.yml | 2 +- .gitignore | 1 + package-lock.json | 2562 +++++++++++++++++++++++++++++ 3 files changed, 2564 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d27a68d..4aeb52c 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - name: Install dependencies diff --git a/.gitignore b/.gitignore index 79e730d..62049d2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ env/secrets.json .cursor/ .codex/ .claude/ +node_modules/ skills-engineering/ios-engineer/evolution/usage diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e580adc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2562 @@ +{ + "name": "@i-stack/ai-coding-kit", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@i-stack/ai-coding-kit", + "version": "3.0.0", + "license": "MIT", + "bin": { + "ai-coding-kit": "skills-engineering/scripts/sync-skills.sh", + "ai-coding-kit-install-hooks": "install-hooks.sh", + "ai-coding-kit-list-skills": "skills-engineering/scripts/list-skills.sh", + "ai-coding-kit-sync": "sync.sh" + }, + "devDependencies": { + "vitepress": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.1.tgz", + "integrity": "sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz", + "integrity": "sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.1.tgz", + "integrity": "sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.1.tgz", + "integrity": "sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.1.tgz", + "integrity": "sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.1.tgz", + "integrity": "sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz", + "integrity": "sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", + "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.1.tgz", + "integrity": "sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.1.tgz", + "integrity": "sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.1.tgz", + "integrity": "sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz", + "integrity": "sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz", + "integrity": "sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz", + "integrity": "sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.89", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.89.tgz", + "integrity": "sha512-hRaCY5s2G5oWAIhc4LCGYn6g6RrwLL4zhoLOT+KUO3joVCxVlZKA+839bv/47Nbe9/ZD4UA6dznZ4XPYcI53wA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", + "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.21.1", + "@algolia/client-abtesting": "5.55.1", + "@algolia/client-analytics": "5.55.1", + "@algolia/client-common": "5.55.1", + "@algolia/client-insights": "5.55.1", + "@algolia/client-personalization": "5.55.1", + "@algolia/client-query-suggestions": "5.55.1", + "@algolia/client-search": "5.55.1", + "@algolia/ingestion": "1.55.1", + "@algolia/monitoring": "1.55.1", + "@algolia/recommend": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.4.tgz", + "integrity": "sha512-GMpwh9+NJ8tSmqwIaVyFRQkiKfBEzQ+k7r7tle4W+kaJ+7wJiB9hFz9BixAomMtenPPSBfM4bZhXozGxhf0uFQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} From 5f3bc25c58f463cb5d4b25bd1490ab529ecd20b4 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:42:58 +0800 Subject: [PATCH 29/30] chore: update .gitignore to include usage.jsonl and ignore other files in usage directory - Modified .gitignore to allow tracking of usage.jsonl while ignoring other files in the usage directory. - Added usage.jsonl as a new file in the skills-engineering/ios-engineer/evolution/usage/ path. --- .gitignore | 3 ++- skills-engineering/ios-engineer/evolution/usage/usage.jsonl | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 skills-engineering/ios-engineer/evolution/usage/usage.jsonl diff --git a/.gitignore b/.gitignore index 62049d2..f4aa597 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ env/secrets.json .codex/ .claude/ node_modules/ -skills-engineering/ios-engineer/evolution/usage +skills-engineering/ios-engineer/evolution/usage/* +!skills-engineering/ios-engineer/evolution/usage/usage.jsonl diff --git a/skills-engineering/ios-engineer/evolution/usage/usage.jsonl b/skills-engineering/ios-engineer/evolution/usage/usage.jsonl new file mode 100644 index 0000000..e69de29 From 6c4e07c3d35edb03ec3dedbc585af4bec5234996 Mon Sep 17 00:00:00 2001 From: stack Date: Sun, 5 Jul 2026 19:43:04 +0800 Subject: [PATCH 30/30] chore: add "type" field to package.json for module support - Updated package.json to include the "type" field set to "module", enabling ES module syntax in the project. --- .../.temp/@localSearchIndexroot.BF8B3Qzi.js | 4 + .../.temp/VPLocalSearchBox.BcKWVt-i.js | 366 + docs/.vitepress/.temp/app.js | 5198 ++++++++ .../.temp/assets/style.ClG4-ikt.css | 5095 ++++++++ docs/.vitepress/.temp/index.md.js | 32 + .../.vitepress/.temp/ios-engineer_index.md.js | 38 + .../.temp/ios-engineer_references.md.js | 19 + .../.temp/ios-engineer_rule-index.md.js | 25 + docs/.vitepress/.temp/package.json | 1 + .../plugin-vue_export-helper.1tPrXgE0.js | 10 + docs/.vitepress/dist/404.html | 24 + docs/.vitepress/dist/assets/app.DVfBX2Do.js | 106 + .../chunks/@localSearchIndexroot.DDvkKcys.js | 4 + .../chunks/VPLocalSearchBox.DoLsN_4M.js | 5343 ++++++++ .../dist/assets/chunks/framework.BcMzFyCJ.js | 10587 ++++++++++++++++ .../dist/assets/chunks/theme.BrkFYulG.js | 3226 +++++ .../dist/assets/index.md.KfzzA0m_.js | 13 + .../dist/assets/index.md.KfzzA0m_.lean.js | 13 + .../inter-italic-cyrillic-ext.r48I6akx.woff2 | Bin 0 -> 43112 bytes .../inter-italic-cyrillic.By2_1cv3.woff2 | Bin 0 -> 31300 bytes .../inter-italic-greek-ext.1u6EdAuj.woff2 | Bin 0 -> 17404 bytes .../assets/inter-italic-greek.DJ8dCoTZ.woff2 | Bin 0 -> 32564 bytes .../inter-italic-latin-ext.CN1xVJS-.woff2 | Bin 0 -> 120840 bytes .../assets/inter-italic-latin.C2AdPX0b.woff2 | Bin 0 -> 74784 bytes .../inter-italic-vietnamese.BSbpV94h.woff2 | Bin 0 -> 14884 bytes .../inter-roman-cyrillic-ext.BBPuwvHQ.woff2 | Bin 0 -> 40488 bytes .../inter-roman-cyrillic.C5lxZ8CY.woff2 | Bin 0 -> 29164 bytes .../inter-roman-greek-ext.CqjqNYQ-.woff2 | Bin 0 -> 16272 bytes .../assets/inter-roman-greek.BBVDIX6e.woff2 | Bin 0 -> 29920 bytes .../inter-roman-latin-ext.4ZJIpNVo.woff2 | Bin 0 -> 110160 bytes .../assets/inter-roman-latin.Di8DUHzh.woff2 | Bin 0 -> 67792 bytes .../inter-roman-vietnamese.BjW4sHH5.woff2 | Bin 0 -> 14072 bytes .../assets/ios-engineer_index.md.DqpOxw5_.js | 29 + .../ios-engineer_index.md.DqpOxw5_.lean.js | 29 + .../ios-engineer_references.md.BquDJ_Fl.js | 13 + ...os-engineer_references.md.BquDJ_Fl.lean.js | 13 + .../ios-engineer_rule-index.md.BQjVtrg9.js | 29 + ...os-engineer_rule-index.md.BQjVtrg9.lean.js | 29 + .../.vitepress/dist/assets/style.ClG4-ikt.css | 5095 ++++++++ docs/.vitepress/dist/hashmap.json | 1 + docs/.vitepress/dist/index.html | 40 + docs/.vitepress/dist/ios-engineer/index.html | 40 + .../dist/ios-engineer/references.html | 27 + .../dist/ios-engineer/rule-index.html | 27 + docs/.vitepress/dist/vp-icons.css | 3 + package.json | 1 + 46 files changed, 35480 insertions(+) create mode 100644 docs/.vitepress/.temp/@localSearchIndexroot.BF8B3Qzi.js create mode 100644 docs/.vitepress/.temp/VPLocalSearchBox.BcKWVt-i.js create mode 100644 docs/.vitepress/.temp/app.js create mode 100644 docs/.vitepress/.temp/assets/style.ClG4-ikt.css create mode 100644 docs/.vitepress/.temp/index.md.js create mode 100644 docs/.vitepress/.temp/ios-engineer_index.md.js create mode 100644 docs/.vitepress/.temp/ios-engineer_references.md.js create mode 100644 docs/.vitepress/.temp/ios-engineer_rule-index.md.js create mode 100644 docs/.vitepress/.temp/package.json create mode 100644 docs/.vitepress/.temp/plugin-vue_export-helper.1tPrXgE0.js create mode 100644 docs/.vitepress/dist/404.html create mode 100644 docs/.vitepress/dist/assets/app.DVfBX2Do.js create mode 100644 docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DDvkKcys.js create mode 100644 docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.DoLsN_4M.js create mode 100644 docs/.vitepress/dist/assets/chunks/framework.BcMzFyCJ.js create mode 100644 docs/.vitepress/dist/assets/chunks/theme.BrkFYulG.js create mode 100644 docs/.vitepress/dist/assets/index.md.KfzzA0m_.js create mode 100644 docs/.vitepress/dist/assets/index.md.KfzzA0m_.lean.js create mode 100644 docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 create mode 100644 docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 create mode 100644 docs/.vitepress/dist/assets/ios-engineer_index.md.DqpOxw5_.js create mode 100644 docs/.vitepress/dist/assets/ios-engineer_index.md.DqpOxw5_.lean.js create mode 100644 docs/.vitepress/dist/assets/ios-engineer_references.md.BquDJ_Fl.js create mode 100644 docs/.vitepress/dist/assets/ios-engineer_references.md.BquDJ_Fl.lean.js create mode 100644 docs/.vitepress/dist/assets/ios-engineer_rule-index.md.BQjVtrg9.js create mode 100644 docs/.vitepress/dist/assets/ios-engineer_rule-index.md.BQjVtrg9.lean.js create mode 100644 docs/.vitepress/dist/assets/style.ClG4-ikt.css create mode 100644 docs/.vitepress/dist/hashmap.json create mode 100644 docs/.vitepress/dist/index.html create mode 100644 docs/.vitepress/dist/ios-engineer/index.html create mode 100644 docs/.vitepress/dist/ios-engineer/references.html create mode 100644 docs/.vitepress/dist/ios-engineer/rule-index.html create mode 100644 docs/.vitepress/dist/vp-icons.css diff --git a/docs/.vitepress/.temp/@localSearchIndexroot.BF8B3Qzi.js b/docs/.vitepress/.temp/@localSearchIndexroot.BF8B3Qzi.js new file mode 100644 index 0000000..ebda80a --- /dev/null +++ b/docs/.vitepress/.temp/@localSearchIndexroot.BF8B3Qzi.js @@ -0,0 +1,4 @@ +const _localSearchIndexroot = '{"documentCount":22,"nextId":22,"documentIds":{"0":"/ai-coding-kit/#quick-start","1":"/ai-coding-kit/#or-install-via-package-manager","2":"/ai-coding-kit/#platform-support","3":"/ai-coding-kit/#modules","4":"/ai-coding-kit/ios-engineer/#ios-engineer","5":"/ai-coding-kit/ios-engineer/#architecture","6":"/ai-coding-kit/ios-engineer/#rule-system","7":"/ai-coding-kit/ios-engineer/#key-rules","8":"/ai-coding-kit/ios-engineer/#ir-001-—-language-anchoring","9":"/ai-coding-kit/ios-engineer/#ir-006-—-version-context-block","10":"/ai-coding-kit/ios-engineer/#ir-011-—-cognitive-adversary-mode","11":"/ai-coding-kit/ios-engineer/#evolution-governance","12":"/ai-coding-kit/ios-engineer/references#references","13":"/ai-coding-kit/ios-engineer/references#governance-layer","14":"/ai-coding-kit/ios-engineer/references#domain-references","15":"/ai-coding-kit/ios-engineer/references#validation-scripts","16":"/ai-coding-kit/ios-engineer/rule-index#rule-index","17":"/ai-coding-kit/ios-engineer/rule-index#iron-rules-ir-nnn","18":"/ai-coding-kit/ios-engineer/rule-index#global-rules-gr-nnn","19":"/ai-coding-kit/ios-engineer/rule-index#symptom-routing-sym-nnn","20":"/ai-coding-kit/ios-engineer/rule-index#task-routing-route-nnn","21":"/ai-coding-kit/ios-engineer/rule-index#output-templates-out-nnn"},"fieldIds":{"title":0,"titles":1,"text":2},"fieldLength":{"0":[2,1,36],"1":[5,2,13],"2":[2,1,39],"3":[1,1,50],"4":[2,1,51],"5":[1,2,68],"6":[2,2,57],"7":[2,2,1],"8":[4,4,12],"9":[5,4,16],"10":[5,4,21],"11":[2,2,50],"12":[1,1,43],"13":[2,1,26],"14":[2,1,43],"15":[2,1,52],"16":[2,1,30],"17":[5,2,35],"18":[5,2,83],"19":[5,2,54],"20":[5,2,24],"21":[5,2,30]},"averageFieldLength":[3.0454545454545454,1.863636363636364,37.90909090909091],"storedFields":{"0":{"title":"Quick Start","titles":[]},"1":{"title":"Or install via package manager","titles":["Quick Start"]},"2":{"title":"Platform Support","titles":[]},"3":{"title":"Modules","titles":[]},"4":{"title":"iOS Engineer","titles":[]},"5":{"title":"Architecture","titles":["iOS Engineer"]},"6":{"title":"Rule System","titles":["iOS Engineer"]},"7":{"title":"Key Rules","titles":["iOS Engineer"]},"8":{"title":"IR-001 — Language Anchoring","titles":["iOS Engineer","Key Rules"]},"9":{"title":"IR-006 — Version Context Block","titles":["iOS Engineer","Key Rules"]},"10":{"title":"IR-011 — Cognitive Adversary Mode","titles":["iOS Engineer","Key Rules"]},"11":{"title":"Evolution Governance","titles":["iOS Engineer"]},"12":{"title":"References","titles":[]},"13":{"title":"Governance Layer","titles":["References"]},"14":{"title":"Domain References","titles":["References"]},"15":{"title":"Validation Scripts","titles":["References"]},"16":{"title":"Rule Index","titles":[]},"17":{"title":"Iron Rules (IR-NNN)","titles":["Rule Index"]},"18":{"title":"Global Rules (GR-NNN)","titles":["Rule Index"]},"19":{"title":"Symptom Routing (SYM-NNN)","titles":["Rule Index"]},"20":{"title":"Task Routing (ROUTE-NNN)","titles":["Rule Index"]},"21":{"title":"Output Templates (OUT-NNN)","titles":["Rule Index"]}},"dirtCount":0,"index":[["jitter",{"2":{"19":1}}],["json",{"2":{"0":3,"2":6}}],["write",{"2":{"19":1}}],["with",{"2":{"15":1,"18":1,"21":1}}],["why",{"2":{"18":1}}],["which",{"2":{"11":1}}],["when",{"2":{"10":1,"18":1}}],["what",{"2":{"2":1}}],["49",{"2":{"13":1}}],["40+",{"2":{"6":1}}],["15",{"2":{"18":1}}],["1",{"2":{"18":2}}],["14",{"2":{"11":1,"15":1}}],["10",{"2":{"6":1,"20":1}}],["010",{"2":{"18":1}}],["011",{"0":{"10":1},"2":{"17":1}}],["008",{"2":{"18":1}}],["007",{"2":{"18":1,"19":1}}],["005",{"2":{"18":1,"19":1}}],["004",{"2":{"18":1,"19":1}}],["003",{"2":{"18":1,"19":1}}],["002",{"2":{"18":1,"19":1}}],["006",{"0":{"9":1},"2":{"17":1,"18":1,"19":1}}],["001",{"0":{"8":1},"2":{"17":1,"18":1,"19":1}}],["knowledge",{"2":{"12":1}}],["key",{"0":{"7":1},"1":{"8":1,"9":1,"10":1}}],["kit",{"2":{"0":2,"1":2,"4":1}}],["6",{"2":{"6":1,"21":1}}],["→",{"2":{"6":2,"18":3,"19":1}}],["7",{"2":{"6":1}}],["9",{"2":{"6":1}}],["5",{"2":{"6":1}}],["27",{"2":{"5":1,"15":1}}],["3",{"2":{"6":1,"18":1}}],["31",{"2":{"5":1}}],["34",{"2":{"5":1,"12":1,"14":1}}],["└──",{"2":{"5":4}}],["│",{"2":{"5":7}}],["├──",{"2":{"5":9}}],["list",{"2":{"19":1}}],["lifecycle",{"2":{"12":1}}],["legacy",{"2":{"19":1}}],["ledger",{"2":{"13":2,"15":2}}],["level",{"2":{"10":1}}],["loaded",{"2":{"12":1}}],["logic",{"2":{"6":1,"18":1}}],["locales",{"2":{"4":1}}],["launch",{"2":{"19":1}}],["lag",{"2":{"19":1}}],["last",{"2":{"15":1}}],["layer",{"0":{"13":1},"2":{"5":1}}],["layered",{"2":{"5":1}}],["language",{"0":{"8":1},"2":{"4":1,"8":2,"17":2}}],["zh",{"2":{"4":1,"5":1}}],["简体中文",{"2":{"4":1}}],["ui",{"2":{"19":1}}],["uikit",{"2":{"4":1}}],["unwrap",{"2":{"19":2}}],["universal",{"2":{"3":1}}],["update",{"2":{"11":1}}],["usage",{"2":{"13":2,"15":2}}],["used",{"2":{"12":1}}],["user",{"2":{"8":1,"17":1}}],["us",{"2":{"4":1,"5":1}}],["root",{"2":{"18":1,"19":1,"21":1}}],["route",{"0":{"20":1},"2":{"6":3,"12":1}}],["routing",{"0":{"19":1,"20":1},"2":{"5":1,"6":2,"12":2,"20":1}}],["runtime",{"2":{"12":1}}],["run",{"2":{"11":1}}],["rule",{"0":{"6":1,"16":1},"1":{"17":1,"18":1,"19":1,"20":1,"21":1},"2":{"5":2,"6":2,"11":1,"13":2,"15":3,"16":3,"21":1}}],["rules",{"0":{"7":1,"17":1,"18":1},"1":{"8":1,"9":1,"10":1},"2":{"4":1,"6":2,"12":1,"13":1}}],["record",{"2":{"21":1}}],["records",{"2":{"14":2}}],["request",{"2":{"19":2}}],["requires",{"2":{"11":1}}],["require",{"2":{"9":1}}],["release",{"2":{"14":2}}],["restatement",{"2":{"10":1,"17":1}}],["registry",{"2":{"5":1,"6":1,"13":1,"16":1,"21":1}}],["refresh",{"2":{"19":1}}],["ref",{"2":{"15":1}}],["referenced",{"2":{"16":1}}],["reference",{"2":{"5":1,"12":1,"13":1,"14":2,"15":1,"18":1}}],["references",{"0":{"12":1,"14":1},"1":{"13":1,"14":1,"15":1},"2":{"5":3,"6":2,"11":2,"12":2}}],["refactoring",{"2":{"4":1}}],["review",{"2":{"4":1,"5":1,"20":1,"21":2}}],["renders",{"2":{"3":1}}],["rag",{"2":{"3":2}}],["dates",{"2":{"15":1}}],["data",{"2":{"3":1,"20":1}}],["diff",{"2":{"18":1}}],["directory",{"2":{"14":1}}],["discipline",{"2":{"6":1}}],["driven",{"2":{"5":1,"11":1}}],["domain",{"0":{"14":1},"2":{"5":2,"12":2,"14":3}}],["dependency",{"2":{"20":1}}],["design",{"2":{"20":1,"21":1}}],["description",{"2":{"3":1,"13":1}}],["declaration",{"2":{"18":1}}],["decision",{"2":{"14":2,"21":1}}],["defined",{"2":{"16":1}}],["definitions",{"2":{"3":1}}],["detailed",{"2":{"12":1}}],["development",{"2":{"4":1}}],["debugging",{"2":{"4":1,"20":1}}],["fear",{"2":{"19":1}}],["four",{"2":{"18":1}}],["force",{"2":{"19":2}}],["forced",{"2":{"8":1}}],["formatting",{"2":{"18":1}}],["formats",{"2":{"3":1,"6":1}}],["for",{"2":{"4":1,"5":1,"6":1,"12":1,"14":1,"16":1,"18":1,"21":2}}],["fix",{"2":{"18":2}}],["first",{"2":{"16":1,"18":1}}],["files",{"2":{"5":1,"12":1,"14":1,"15":2}}],["file",{"2":{"0":1}}],["freshness",{"2":{"15":1}}],["full",{"2":{"12":1,"14":1}}],["flip",{"2":{"10":1}}],["falsifiability",{"2":{"17":1}}],["falsifiable",{"2":{"10":1}}],["failures",{"2":{"18":1}}],["failure",{"2":{"10":1,"19":1}}],["fastify",{"2":{"3":1}}],["+",{"2":{"2":1,"3":3,"18":1}}],["against",{"2":{"15":1}}],["agent",{"2":{"2":1,"3":1,"4":1,"12":1}}],["auth",{"2":{"19":1}}],["automated",{"2":{"16":1}}],["auto",{"2":{"4":1,"5":1,"6":2,"13":1}}],["audits",{"2":{"15":1}}],["audit",{"2":{"15":1}}],["app",{"2":{"14":2}}],["api",{"2":{"3":1}}],["amp",{"2":{"14":2,"20":2}}],["at",{"2":{"12":1}}],["add",{"2":{"11":1}}],["adversary",{"0":{"10":1},"2":{"13":2,"17":1}}],["are",{"2":{"11":1,"12":2,"15":1}}],["argument",{"2":{"10":1,"17":1}}],["archived",{"2":{"5":1}}],["archive",{"2":{"5":1,"11":1}}],["architecture",{"0":{"5":1},"2":{"4":1,"14":4,"20":1,"21":1}}],["anchor",{"2":{"21":1}}],["anchors",{"2":{"17":1}}],["anchoring",{"0":{"8":1}}],["an",{"2":{"16":1}}],["anti",{"2":{"14":2}}],["and",{"2":{"14":2,"15":2,"21":2}}],["analysis",{"2":{"14":2,"21":1}}],["answers",{"2":{"9":1}}],["availability",{"2":{"9":1,"17":1}}],["all",{"2":{"9":1,"11":1,"14":1}}],["always",{"2":{"6":1}}],["access",{"2":{"19":1}}],["across",{"2":{"6":1}}],["active",{"2":{"5":1,"17":3,"18":9,"19":7}}],["a",{"2":{"5":1,"9":1,"11":3}}],["async",{"2":{"19":1}}],["assertion",{"2":{"19":1}}],["assumptions",{"2":{"10":1,"17":1}}],["assistant",{"2":{"2":1}}],["as",{"2":{"5":1}}],["ai",{"2":{"0":2,"1":2,"4":2,"12":1}}],["xmcp",{"2":{"3":1}}],["xcode",{"2":{"2":1,"4":1}}],["x26",{"2":{"0":1,"5":1}}],["yaml",{"2":{"2":1}}],["you",{"2":{"0":1}}],["your",{"2":{"0":1,"4":1}}],["verify",{"2":{"18":1}}],["verified",{"2":{"15":1}}],["version",{"0":{"9":1},"2":{"9":1,"17":1}}],["validates",{"2":{"15":2}}],["validate",{"2":{"11":2,"15":4,"16":1}}],["validation",{"0":{"15":1},"2":{"5":1,"15":2,"16":1}}],["variables",{"2":{"2":1}}],["vscode",{"2":{"2":1}}],["via",{"0":{"1":1}}],["memory",{"2":{"19":1}}],["misalignment",{"2":{"19":1}}],["minimal",{"2":{"18":1}}],["mirrors",{"2":{"5":1,"18":1}}],["migration",{"2":{"4":1,"20":1,"21":1}}],["md",{"2":{"5":3,"11":2,"13":4,"14":9,"15":2,"16":1,"21":1}}],["markdown",{"2":{"20":1}}],["max",{"2":{"18":1}}],["matches",{"2":{"4":1,"8":1}}],["manager",{"0":{"1":1}}],["multi",{"2":{"3":1}}],["modify",{"2":{"11":1}}],["modeling",{"2":{"14":2}}],["models",{"2":{"2":1}}],["mode",{"0":{"10":1},"2":{"13":2,"17":1}}],["module",{"2":{"3":1}}],["modules",{"0":{"3":1},"2":{"19":1}}],["mcp",{"2":{"2":4,"3":2}}],["plan",{"2":{"21":1}}],["platform",{"0":{"2":1},"2":{"3":2,"6":1,"18":1}}],["persistence",{"2":{"20":1}}],["permission",{"2":{"20":1}}],["performance",{"2":{"4":1}}],["purpose",{"2":{"15":1}}],["push",{"2":{"3":2}}],["position",{"2":{"10":1}}],["points",{"2":{"21":1}}],["point",{"2":{"5":1}}],["pipeline",{"2":{"5":1,"11":1}}],["provide",{"2":{"12":1}}],["providing",{"2":{"4":1}}],["promote",{"2":{"11":1}}],["propose",{"2":{"11":1}}],["proposals",{"2":{"5":3,"11":1}}],["proposal",{"2":{"5":1,"11":4}}],["production",{"2":{"4":1}}],["project",{"2":{"3":1,"19":1}}],["primary",{"2":{"4":1,"18":1}}],["prevents",{"2":{"18":1}}],["prefix",{"2":{"6":1}}],["pre",{"2":{"3":2,"11":1,"18":1}}],["pagination",{"2":{"19":1}}],["patterns",{"2":{"14":2}}],["paths",{"2":{"2":1}}],["package",{"0":{"1":1}}],["gate",{"2":{"18":1}}],["gated",{"2":{"11":1}}],["gateway",{"2":{"3":2}}],["gr",{"0":{"18":1},"2":{"6":1,"18":9}}],["grade",{"2":{"4":1}}],["global",{"0":{"18":1},"2":{"6":1,"18":1}}],["guard",{"2":{"5":1}}],["guards",{"2":{"3":1}}],["governance",{"0":{"11":1,"13":1},"2":{"5":2,"13":1}}],["governed",{"2":{"3":1}}],["generation",{"2":{"20":1}}],["generated",{"2":{"2":1}}],["gemini",{"2":{"2":1}}],["gets",{"2":{"2":1}}],["g",{"2":{"1":1}}],["githooks",{"2":{"3":1}}],["github",{"2":{"0":1,"14":1}}],["git",{"2":{"0":2,"3":1}}],["naming",{"2":{"19":1}}],["native",{"2":{"3":1}}],["noise",{"2":{"18":1}}],["no",{"2":{"8":1,"18":1}}],["nnn",{"0":{"17":1,"18":1,"19":1,"20":1,"21":1},"2":{"6":5}}],["never",{"2":{"18":1}}],["network",{"2":{"9":1,"14":1,"17":1}}],["networking",{"2":{"4":1,"14":1}}],["need",{"2":{"0":1}}],["npm",{"2":{"1":2}}],["back",{"2":{"19":1}}],["based",{"2":{"12":1}}],["bash",{"2":{"0":2,"1":1}}],["budget",{"2":{"18":1}}],["build",{"2":{"14":2,"20":1}}],["bidirectional",{"2":{"16":1}}],["by",{"2":{"11":1,"12":1,"18":1}}],["between",{"2":{"15":1}}],["before",{"2":{"9":1,"17":1}}],["behavior",{"2":{"9":1}}],["blocks",{"2":{"18":1}}],["block",{"0":{"9":1},"2":{"9":1,"17":1,"18":1}}],["brew",{"2":{"1":1}}],["here",{"2":{"16":1}}],["hidden",{"2":{"10":1,"17":1}}],["how",{"2":{"12":1}}],["hook",{"2":{"11":1}}],["hooks",{"2":{"3":2,"5":1}}],["homebrew",{"2":{"1":1}}],["https",{"2":{"0":1}}],["of",{"2":{"19":1}}],["override",{"2":{"19":1}}],["out",{"0":{"21":1},"2":{"6":1}}],["output",{"0":{"21":1},"2":{"5":1,"6":2,"8":2,"10":1,"17":1,"18":1,"21":1}}],["openai",{"2":{"3":1}}],["organized",{"2":{"5":1}}],["or",{"0":{"1":1},"2":{"11":1,"12":1,"18":1}}],["on",{"2":{"12":1,"14":1,"17":1}}],["one",{"2":{"0":1}}],["only",{"2":{"0":1}}],["$editor",{"2":{"0":1}}],["expose",{"2":{"18":1}}],["extensions",{"2":{"14":2}}],["examples",{"2":{"14":2}}],["example",{"2":{"0":1}}],["epistemic",{"2":{"6":1}}],["etc",{"2":{"3":1}}],["every",{"2":{"16":1}}],["everything",{"2":{"0":1}}],["evolves",{"2":{"11":1}}],["evolution",{"0":{"11":1},"2":{"3":1,"5":7,"11":3,"13":2,"15":3}}],["entries",{"2":{"20":1}}],["entry",{"2":{"5":1}}],["energy",{"2":{"19":1}}],["ensures",{"2":{"15":1,"16":1}}],["enforcement",{"2":{"19":1}}],["enforced",{"2":{"6":1}}],["enforces",{"2":{"6":1}}],["en",{"2":{"4":1,"5":1}}],["english",{"2":{"4":1,"5":1}}],["engineer",{"0":{"4":1},"1":{"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1},"2":{"5":1,"12":1,"16":1,"18":1}}],["engineering",{"2":{"3":1,"4":1,"12":1}}],["engine",{"2":{"3":1}}],["environment",{"2":{"2":1}}],["env",{"2":{"0":3,"3":1}}],["edit",{"2":{"0":1}}],["turns",{"2":{"18":1}}],["traceable",{"2":{"18":1}}],["tracking",{"2":{"13":1}}],["triggered",{"2":{"10":1}}],["triggers",{"2":{"5":1}}],["types",{"2":{"6":1}}],["typescript",{"2":{"3":1}}],["task",{"0":{"20":1},"2":{"6":2,"12":1}}],["tap",{"2":{"1":1}}],["test",{"2":{"21":1}}],["testing",{"2":{"4":1,"20":1}}],["templates",{"0":{"21":1},"2":{"5":1,"6":1,"14":2,"21":1}}],["through",{"2":{"11":1}}],["this",{"2":{"4":1}}],["them",{"2":{"18":1}}],["then",{"2":{"16":1}}],["they",{"2":{"12":1}}],["the",{"2":{"0":1,"4":2,"5":1,"6":3,"8":1,"11":4,"12":3,"14":1,"15":1,"16":1,"18":1,"21":2}}],["toml",{"2":{"2":2}}],["tool",{"2":{"2":1,"18":1}}],["touching",{"2":{"19":1}}],["touch",{"2":{"0":1}}],["to",{"2":{"0":2,"3":1,"11":1,"17":1}}],["slow",{"2":{"19":1}}],["single",{"2":{"18":1}}],["summary",{"2":{"17":1,"18":1,"19":1}}],["supported",{"2":{"4":1}}],["support",{"0":{"2":1}}],["same",{"2":{"11":1}}],["snapshot",{"2":{"11":1,"15":1}}],["snapshots",{"2":{"5":2,"15":1}}],["s",{"2":{"8":1,"17":1}}],["step",{"2":{"11":1,"15":1}}],["strength",{"2":{"18":1}}],["strongest",{"2":{"10":1}}],["structured",{"2":{"6":1}}],["stale",{"2":{"19":1}}],["status",{"2":{"17":1,"18":1,"19":1,"21":1}}],["state",{"2":{"11":1,"15":1,"19":1}}],["staged",{"2":{"11":1}}],["stack",{"2":{"0":1,"1":2}}],["start",{"0":{"0":1},"1":{"1":1}}],["scenario",{"2":{"15":2}}],["scenarios",{"2":{"12":1}}],["script",{"2":{"15":1,"16":1}}],["scripts",{"0":{"15":1},"2":{"5":3,"11":1,"15":2}}],["scope",{"2":{"6":1}}],["specs",{"2":{"15":1}}],["specification",{"2":{"13":1,"15":1}}],["specific",{"2":{"5":1,"12":1}}],["spm",{"2":{"4":1}}],["sym",{"0":{"19":1},"2":{"6":1,"12":1,"19":7}}],["symptoms",{"2":{"6":1}}],["symptom",{"0":{"19":1},"2":{"6":1,"12":1}}],["system",{"0":{"6":1},"2":{"5":1}}],["synced",{"2":{"2":1}}],["sync",{"2":{"0":2,"3":3}}],["swiftui",{"2":{"4":1,"9":1,"17":1}}],["swift",{"2":{"4":1,"12":1}}],["source",{"2":{"3":1}}],["skill",{"2":{"3":1,"4":2,"5":2,"6":1,"11":4,"12":1,"15":4,"16":1,"18":1}}],["skills",{"2":{"2":3,"3":1,"18":1,"20":1}}],["section",{"2":{"18":1}}],["secondary",{"2":{"18":1}}],["security",{"2":{"18":1,"20":1}}],["secrets",{"2":{"0":4,"3":2}}],["see",{"2":{"6":1,"14":1,"21":1}}],["self",{"2":{"5":1,"10":1,"13":1}}],["settings",{"2":{"2":2}}],["ships",{"2":{"15":1}}],["sh",{"2":{"0":1,"11":1,"15":6,"16":1}}],["implement",{"2":{"11":1}}],["implemented",{"2":{"5":1}}],["ir",{"0":{"8":1,"9":1,"10":1,"17":1},"2":{"6":1,"17":3}}],["iron",{"0":{"17":1},"2":{"6":1}}],["i18n",{"2":{"5":1}}],["ids",{"2":{"6":1,"13":1,"15":2,"16":1}}],["id",{"2":{"5":1,"13":1,"16":2,"17":1,"18":1,"19":1}}],["is",{"2":{"4":1,"5":1,"16":1,"18":1}}],["ios",{"0":{"4":1},"1":{"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1},"2":{"4":2,"5":1,"12":2,"16":1,"18":1}}],["indicators",{"2":{"18":1}}],["independent",{"2":{"18":1}}],["index",{"0":{"16":1},"1":{"17":1,"18":1,"19":1,"20":1,"21":1},"2":{"5":1,"6":1,"11":1,"13":1,"15":1,"21":1}}],["insufficient",{"2":{"18":1}}],["install",{"0":{"1":1},"2":{"1":2}}],["info",{"2":{"18":1}}],["integrity",{"2":{"15":1}}],["includes",{"2":{"12":1}}],["input",{"2":{"8":1,"17":1}}],["in",{"2":{"4":1,"5":1,"11":2,"15":2,"16":1}}],["init",{"2":{"3":1}}],["injects",{"2":{"3":1}}],["i",{"2":{"0":1,"1":2}}],["current",{"2":{"15":1}}],["cursor",{"2":{"2":2}}],["ci",{"2":{"14":2,"20":1}}],["chaos",{"2":{"19":3}}],["chain",{"2":{"18":1}}],["change",{"2":{"18":1}}],["changes",{"2":{"11":1}}],["check",{"2":{"10":1,"11":1,"15":1}}],["checks",{"2":{"5":1}}],["chinese",{"2":{"8":1}}],["credentials",{"2":{"18":1}}],["create",{"2":{"11":1}}],["cross",{"2":{"6":1,"18":1}}],["crash",{"2":{"4":1,"19":1}}],["cause",{"2":{"18":2,"19":1,"21":1}}],["carried",{"2":{"18":1}}],["cancellation",{"2":{"9":1}}],["canonical",{"2":{"5":1,"13":1,"16":1,"21":1}}],["category",{"2":{"6":1}}],["categories",{"2":{"6":1}}],["cn",{"2":{"4":1,"5":1}}],["cline",{"2":{"2":1}}],["cli",{"2":{"2":2}}],["claude",{"2":{"2":3}}],["clone",{"2":{"0":2}}],["cp",{"2":{"0":1}}],["cd",{"2":{"0":1}}],["core",{"2":{"20":1}}],["coverage",{"2":{"18":1}}],["covering",{"2":{"12":1,"20":1}}],["cognitive",{"0":{"10":1},"2":{"13":2,"17":1}}],["counter",{"2":{"10":1,"17":1}}],["count",{"2":{"6":1}}],["cocoapods",{"2":{"4":1}}],["constraint",{"2":{"19":1}}],["consistent",{"2":{"15":1}}],["consistency",{"2":{"5":1,"15":1,"16":1}}],["conflicts",{"2":{"19":1}}],["confirmation",{"2":{"18":1}}],["confidence",{"2":{"10":1}}],["configs",{"2":{"3":1}}],["config",{"2":{"2":3,"3":2}}],["configure",{"2":{"0":1}}],["conformity",{"2":{"10":1}}],["conditions",{"2":{"10":2}}],["conclusion",{"2":{"10":1}}],["conclusions",{"2":{"9":1,"17":1}}],["concurrency",{"2":{"4":1,"9":1,"17":1}}],["control",{"2":{"19":1}}],["context",{"0":{"9":1},"2":{"9":1,"17":1}}],["content",{"2":{"3":1}}],["continue",{"2":{"2":2}}],["codex",{"2":{"2":3}}],["code",{"2":{"2":1,"4":1,"14":2,"18":1,"20":2,"21":1}}],["codebuddy",{"2":{"2":2}}],["coding",{"2":{"0":2,"1":2,"2":1,"4":2}}],["compliance",{"2":{"18":1}}],["complete",{"2":{"6":1,"21":1}}],["compares",{"2":{"15":1}}],["compatible",{"2":{"3":1}}],["comprehensive",{"2":{"15":1}}],["commit",{"2":{"3":2,"11":2}}],["command",{"2":{"0":1}}],["com",{"2":{"0":1}}],["quick",{"0":{"0":1},"1":{"1":1}}]],"serializationVersion":2}'; +export { + _localSearchIndexroot as default +}; diff --git a/docs/.vitepress/.temp/VPLocalSearchBox.BcKWVt-i.js b/docs/.vitepress/.temp/VPLocalSearchBox.BcKWVt-i.js new file mode 100644 index 0000000..7b683d1 --- /dev/null +++ b/docs/.vitepress/.temp/VPLocalSearchBox.BcKWVt-i.js @@ -0,0 +1,366 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { defineComponent, shallowRef, markRaw, computed, ref, watchEffect, watch, createApp, nextTick, onMounted, onBeforeUnmount, unref, useSSRContext } from "vue"; +import { ssrRenderTeleport, ssrRenderAttr, ssrRenderClass, ssrIncludeBooleanAttr, ssrRenderList, ssrInterpolate } from "vue/server-renderer"; +import { computedAsync, useSessionStorage, useLocalStorage, debouncedWatch, onKeyStroke, useEventListener, useScrollLock } from "@vueuse/core"; +import { useFocusTrap } from "@vueuse/integrations/useFocusTrap"; +import Mark from "mark.js/src/vanilla.js"; +import MiniSearch from "minisearch"; +import { u as useData, d as dataSymbol, p as pathToFile, a as useRouter, c as createSearchTranslate, i as inBrowser, e as escapeRegExp } from "./app.js"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const localSearchIndex = { "root": () => import("./@localSearchIndexroot.BF8B3Qzi.js") }; +class LRUCache { + constructor(max = 10) { + __publicField(this, "max"); + __publicField(this, "cache"); + this.max = max; + this.cache = /* @__PURE__ */ new Map(); + } + get(key) { + let item = this.cache.get(key); + if (item !== void 0) { + this.cache.delete(key); + this.cache.set(key, item); + } + return item; + } + set(key, val) { + if (this.cache.has(key)) + this.cache.delete(key); + else if (this.cache.size === this.max) + this.cache.delete(this.first()); + this.cache.set(key, val); + } + first() { + return this.cache.keys().next().value; + } + clear() { + this.cache.clear(); + } +} +const _sfc_main = /* @__PURE__ */ defineComponent({ + __name: "VPLocalSearchBox", + __ssrInlineRender: true, + emits: ["close"], + setup(__props, { emit: __emit }) { + var _a, _b; + const emit = __emit; + const el = shallowRef(); + const resultsEl = shallowRef(); + const searchIndexData = shallowRef(localSearchIndex); + const vitePressData = useData(); + const { activate } = useFocusTrap(el, { + immediate: true, + allowOutsideClick: true, + clickOutsideDeactivates: true, + escapeDeactivates: true + }); + const { localeIndex, theme } = vitePressData; + const searchIndex = computedAsync( + async () => { + var _a2, _b2, _c, _d, _e, _f, _g, _h, _i; + return markRaw( + MiniSearch.loadJSON( + (_c = await ((_b2 = (_a2 = searchIndexData.value)[localeIndex.value]) == null ? void 0 : _b2.call(_a2))) == null ? void 0 : _c.default, + { + fields: ["title", "titles", "text"], + storeFields: ["title", "titles"], + searchOptions: { + fuzzy: 0.2, + prefix: true, + boost: { title: 4, text: 2, titles: 1 }, + ...((_d = theme.value.search) == null ? void 0 : _d.provider) === "local" && ((_f = (_e = theme.value.search.options) == null ? void 0 : _e.miniSearch) == null ? void 0 : _f.searchOptions) + }, + ...((_g = theme.value.search) == null ? void 0 : _g.provider) === "local" && ((_i = (_h = theme.value.search.options) == null ? void 0 : _h.miniSearch) == null ? void 0 : _i.options) + } + ) + ); + } + ); + const disableQueryPersistence = computed(() => { + var _a2, _b2; + return ((_a2 = theme.value.search) == null ? void 0 : _a2.provider) === "local" && ((_b2 = theme.value.search.options) == null ? void 0 : _b2.disableQueryPersistence) === true; + }); + const filterText = disableQueryPersistence.value ? ref("") : useSessionStorage("vitepress:local-search-filter", ""); + const showDetailedList = useLocalStorage( + "vitepress:local-search-detailed-list", + ((_a = theme.value.search) == null ? void 0 : _a.provider) === "local" && ((_b = theme.value.search.options) == null ? void 0 : _b.detailedView) === true + ); + const disableDetailedView = computed(() => { + var _a2, _b2, _c; + return ((_a2 = theme.value.search) == null ? void 0 : _a2.provider) === "local" && (((_b2 = theme.value.search.options) == null ? void 0 : _b2.disableDetailedView) === true || ((_c = theme.value.search.options) == null ? void 0 : _c.detailedView) === false); + }); + const buttonText = computed(() => { + var _a2, _b2, _c, _d, _e, _f, _g; + const options = ((_a2 = theme.value.search) == null ? void 0 : _a2.options) ?? theme.value.algolia; + return ((_e = (_d = (_c = (_b2 = options == null ? void 0 : options.locales) == null ? void 0 : _b2[localeIndex.value]) == null ? void 0 : _c.translations) == null ? void 0 : _d.button) == null ? void 0 : _e.buttonText) || ((_g = (_f = options == null ? void 0 : options.translations) == null ? void 0 : _f.button) == null ? void 0 : _g.buttonText) || "Search"; + }); + watchEffect(() => { + if (disableDetailedView.value) { + showDetailedList.value = false; + } + }); + const results = shallowRef([]); + const enableNoResults = ref(false); + watch(filterText, () => { + enableNoResults.value = false; + }); + const mark = computedAsync(async () => { + if (!resultsEl.value) return; + return markRaw(new Mark(resultsEl.value)); + }, null); + const cache = new LRUCache(16); + debouncedWatch( + () => [searchIndex.value, filterText.value, showDetailedList.value], + async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { + var _a2, _b2, _c, _d; + if ((old == null ? void 0 : old[0]) !== index) { + cache.clear(); + } + let canceled = false; + onCleanup(() => { + canceled = true; + }); + if (!index) return; + results.value = index.search(filterTextValue).slice(0, 16); + enableNoResults.value = true; + const mods = showDetailedListValue ? await Promise.all(results.value.map((r) => fetchExcerpt(r.id))) : []; + if (canceled) return; + for (const { id, mod } of mods) { + const mapId = id.slice(0, id.indexOf("#")); + let map = cache.get(mapId); + if (map) continue; + map = /* @__PURE__ */ new Map(); + cache.set(mapId, map); + const comp = mod.default ?? mod; + if ((comp == null ? void 0 : comp.render) || (comp == null ? void 0 : comp.setup)) { + const app = createApp(comp); + app.config.warnHandler = () => { + }; + app.provide(dataSymbol, vitePressData); + Object.defineProperties(app.config.globalProperties, { + $frontmatter: { + get() { + return vitePressData.frontmatter.value; + } + }, + $params: { + get() { + return vitePressData.page.value.params; + } + } + }); + const div = document.createElement("div"); + app.mount(div); + const headings = div.querySelectorAll("h1, h2, h3, h4, h5, h6"); + headings.forEach((el2) => { + var _a3; + const href = (_a3 = el2.querySelector("a")) == null ? void 0 : _a3.getAttribute("href"); + const anchor = (href == null ? void 0 : href.startsWith("#")) && href.slice(1); + if (!anchor) return; + let html = ""; + while ((el2 = el2.nextElementSibling) && !/^h[1-6]$/i.test(el2.tagName)) + html += el2.outerHTML; + map.set(anchor, html); + }); + app.unmount(); + } + if (canceled) return; + } + const terms = /* @__PURE__ */ new Set(); + results.value = results.value.map((r) => { + const [id, anchor] = r.id.split("#"); + const map = cache.get(id); + const text = (map == null ? void 0 : map.get(anchor)) ?? ""; + for (const term in r.match) { + terms.add(term); + } + return { ...r, text }; + }); + await nextTick(); + if (canceled) return; + await new Promise((r) => { + var _a3; + (_a3 = mark.value) == null ? void 0 : _a3.unmark({ + done: () => { + var _a4; + (_a4 = mark.value) == null ? void 0 : _a4.markRegExp(formMarkRegex(terms), { done: r }); + } + }); + }); + const excerpts = ((_a2 = el.value) == null ? void 0 : _a2.querySelectorAll(".result .excerpt")) ?? []; + for (const excerpt of excerpts) { + (_b2 = excerpt.querySelector('mark[data-markjs="true"]')) == null ? void 0 : _b2.scrollIntoView({ block: "center" }); + } + (_d = (_c = resultsEl.value) == null ? void 0 : _c.firstElementChild) == null ? void 0 : _d.scrollIntoView({ block: "start" }); + }, + { debounce: 200, immediate: true } + ); + async function fetchExcerpt(id) { + const file = pathToFile(id.slice(0, id.indexOf("#"))); + try { + if (!file) throw new Error(`Cannot find file for id: ${id}`); + return { id, mod: await import( + /*@vite-ignore*/ + file + ) }; + } catch (e) { + console.error(e); + return { id, mod: {} }; + } + } + const searchInput = ref(); + const disableReset = computed(() => { + var _a2; + return ((_a2 = filterText.value) == null ? void 0 : _a2.length) <= 0; + }); + function focusSearchInput(select = true) { + var _a2, _b2; + (_a2 = searchInput.value) == null ? void 0 : _a2.focus(); + select && ((_b2 = searchInput.value) == null ? void 0 : _b2.select()); + } + onMounted(() => { + focusSearchInput(); + }); + const selectedIndex = ref(-1); + const disableMouseOver = ref(true); + watch(results, (r) => { + selectedIndex.value = r.length ? 0 : -1; + scrollToSelectedResult(); + }); + function scrollToSelectedResult() { + nextTick(() => { + const selectedEl = document.querySelector(".result.selected"); + selectedEl == null ? void 0 : selectedEl.scrollIntoView({ block: "nearest" }); + }); + } + onKeyStroke("ArrowUp", (event) => { + event.preventDefault(); + selectedIndex.value--; + if (selectedIndex.value < 0) { + selectedIndex.value = results.value.length - 1; + } + disableMouseOver.value = true; + scrollToSelectedResult(); + }); + onKeyStroke("ArrowDown", (event) => { + event.preventDefault(); + selectedIndex.value++; + if (selectedIndex.value >= results.value.length) { + selectedIndex.value = 0; + } + disableMouseOver.value = true; + scrollToSelectedResult(); + }); + const router = useRouter(); + onKeyStroke("Enter", (e) => { + if (e.isComposing) return; + if (e.target instanceof HTMLButtonElement && e.target.type !== "submit") + return; + const selectedPackage = results.value[selectedIndex.value]; + if (e.target instanceof HTMLInputElement && !selectedPackage) { + e.preventDefault(); + return; + } + if (selectedPackage) { + router.go(selectedPackage.id); + emit("close"); + } + }); + onKeyStroke("Escape", () => { + emit("close"); + }); + const defaultTranslations = { + modal: { + displayDetails: "Display detailed list", + resetButtonTitle: "Reset search", + backButtonTitle: "Close search", + noResultsText: "No results for", + footer: { + selectText: "to select", + selectKeyAriaLabel: "enter", + navigateText: "to navigate", + navigateUpKeyAriaLabel: "up arrow", + navigateDownKeyAriaLabel: "down arrow", + closeText: "to close", + closeKeyAriaLabel: "escape" + } + } + }; + const translate = createSearchTranslate(defaultTranslations); + onMounted(() => { + window.history.pushState(null, "", null); + }); + useEventListener("popstate", (event) => { + event.preventDefault(); + emit("close"); + }); + const isLocked = useScrollLock(inBrowser ? document.body : null); + onMounted(() => { + nextTick(() => { + isLocked.value = true; + nextTick().then(() => activate()); + }); + }); + onBeforeUnmount(() => { + isLocked.value = false; + }); + function formMarkRegex(terms) { + return new RegExp( + [...terms].sort((a, b) => b.length - a.length).map((term) => `(${escapeRegExp(term)})`).join("|"), + "gi" + ); + } + return (_ctx, _push, _parent, _attrs) => { + ssrRenderTeleport(_push, (_push2) => { + var _a2, _b2, _c, _d, _e; + _push2(`
`); + }, "body", false, _parent); + }; + } +}); +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPLocalSearchBox.vue"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const VPLocalSearchBox = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-ce626c7c"]]); +export { + VPLocalSearchBox as default +}; diff --git a/docs/.vitepress/.temp/app.js b/docs/.vitepress/.temp/app.js new file mode 100644 index 0000000..d52c080 --- /dev/null +++ b/docs/.vitepress/.temp/app.js @@ -0,0 +1,5198 @@ +import { ssrRenderAttrs, ssrRenderSlot, ssrInterpolate, ssrRenderAttr, ssrRenderList, ssrRenderComponent, ssrRenderVNode, ssrRenderClass, renderToString } from "vue/server-renderer"; +import { defineComponent, mergeProps, useSSRContext, shallowRef, inject, computed, ref, watch, onUnmounted, reactive, markRaw, readonly, nextTick, h, unref, onMounted, watchEffect, watchPostEffect, onUpdated, resolveComponent, createVNode, resolveDynamicComponent, withCtx, renderSlot, createTextVNode, toDisplayString, openBlock, createBlock, createCommentVNode, Fragment, renderList, defineAsyncComponent, provide, toHandlers, withKeys, onBeforeUnmount, useSlots, createSSRApp } from "vue"; +import { usePreferredDark, useDark, useMediaQuery, useWindowSize, onKeyStroke, useWindowScroll, useScrollLock } from "@vueuse/core"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const _sfc_main$14 = /* @__PURE__ */ defineComponent({ + __name: "VPBadge", + __ssrInlineRender: true, + props: { + text: {}, + type: { default: "tip" } + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderSlot(_ctx.$slots, "default", {}, () => { + _push(`${ssrInterpolate(__props.text)}`); + }, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$14 = _sfc_main$14.setup; +_sfc_main$14.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue"); + return _sfc_setup$14 ? _sfc_setup$14(props, ctx) : void 0; +}; +function deserializeFunctions(r) { + return Array.isArray(r) ? r.map(deserializeFunctions) : typeof r == "object" && r !== null ? Object.keys(r).reduce((t, n) => (t[n] = deserializeFunctions(r[n]), t), {}) : typeof r == "string" && r.startsWith("_vp-fn_") ? new Function(`return ${r.slice(7)}`)() : r; +} +const siteData = deserializeFunctions(JSON.parse('{"lang":"en-US","dir":"ltr","title":"ai-coding-kit","description":"One kit for all AI coding tools — Agent Skills, MCP sync, iOS engineering rules, and RAG gateway","base":"/ai-coding-kit/","head":[],"router":{"prefetchLinks":true},"appearance":true,"themeConfig":{"logo":false,"siteTitle":"ai-coding-kit","nav":[{"text":"Home","link":"/"},{"text":"iOS Engineer","link":"/ios-engineer/"},{"text":"GitHub","link":"https://github.com/i-stack/ai-coding-kit"}],"sidebar":{"/ios-engineer/":[{"text":"iOS Engineer","collapsed":false,"items":[{"text":"Overview","link":"/ios-engineer/"},{"text":"Rule Index","link":"/ios-engineer/rule-index"},{"text":"References","link":"/ios-engineer/references"}]}]},"socialLinks":[{"icon":"github","link":"https://github.com/i-stack/ai-coding-kit"}],"footer":{"message":"Released under the MIT License.","copyright":"Copyright © 2025–2026 i-stack"},"search":{"provider":"local"},"editLink":{"pattern":"https://github.com/i-stack/ai-coding-kit/edit/feature_3.0.0/docs/:path"}},"locales":{},"scrollOffset":134,"cleanUrls":true}')); +const __vite_import_meta_env__ = {}; +const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i; +const APPEARANCE_KEY = "vitepress-theme-appearance"; +const HASH_RE = /#.*$/; +const HASH_OR_QUERY_RE = /[?#].*$/; +const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/; +const inBrowser = typeof document !== "undefined"; +const notFoundPageData = { + relativePath: "404.md", + filePath: "", + title: "404", + description: "Not Found", + headers: [], + frontmatter: { sidebar: false, layout: "page" }, + lastUpdated: 0, + isNotFound: true +}; +function isActive(currentPath, matchPath, asRegex = false) { + if (matchPath === void 0) { + return false; + } + currentPath = normalize(`/${currentPath}`); + if (asRegex) { + return new RegExp(matchPath).test(currentPath); + } + if (normalize(matchPath) !== currentPath) { + return false; + } + const hashMatch = matchPath.match(HASH_RE); + if (hashMatch) { + return (inBrowser ? location.hash : "") === hashMatch[0]; + } + return true; +} +function normalize(path) { + return decodeURI(path).replace(HASH_OR_QUERY_RE, "").replace(INDEX_OR_EXT_RE, "$1"); +} +function isExternal(path) { + return EXTERNAL_URL_RE.test(path); +} +function getLocaleForPath(siteData2, relativePath) { + return Object.keys((siteData2 == null ? void 0 : siteData2.locales) || {}).find((key) => key !== "root" && !isExternal(key) && isActive(relativePath, `/${key}/`, true)) || "root"; +} +function resolveSiteDataByRoute(siteData2, relativePath) { + var _a, _b, _c, _d, _e, _f, _g; + const localeIndex = getLocaleForPath(siteData2, relativePath); + return Object.assign({}, siteData2, { + localeIndex, + lang: ((_a = siteData2.locales[localeIndex]) == null ? void 0 : _a.lang) ?? siteData2.lang, + dir: ((_b = siteData2.locales[localeIndex]) == null ? void 0 : _b.dir) ?? siteData2.dir, + title: ((_c = siteData2.locales[localeIndex]) == null ? void 0 : _c.title) ?? siteData2.title, + titleTemplate: ((_d = siteData2.locales[localeIndex]) == null ? void 0 : _d.titleTemplate) ?? siteData2.titleTemplate, + description: ((_e = siteData2.locales[localeIndex]) == null ? void 0 : _e.description) ?? siteData2.description, + head: mergeHead(siteData2.head, ((_f = siteData2.locales[localeIndex]) == null ? void 0 : _f.head) ?? []), + themeConfig: { + ...siteData2.themeConfig, + ...(_g = siteData2.locales[localeIndex]) == null ? void 0 : _g.themeConfig + } + }); +} +function createTitle(siteData2, pageData) { + const title = pageData.title || siteData2.title; + const template = pageData.titleTemplate ?? siteData2.titleTemplate; + if (typeof template === "string" && template.includes(":title")) { + return template.replace(/:title/g, title); + } + const templateString = createTitleTemplate(siteData2.title, template); + if (title === templateString.slice(3)) { + return title; + } + return `${title}${templateString}`; +} +function createTitleTemplate(siteTitle, template) { + if (template === false) { + return ""; + } + if (template === true || template === void 0) { + return ` | ${siteTitle}`; + } + if (siteTitle === template) { + return ""; + } + return ` | ${template}`; +} +function hasTag(head, tag) { + const [tagType, tagAttrs] = tag; + if (tagType !== "meta") + return false; + const keyAttr = Object.entries(tagAttrs)[0]; + if (keyAttr == null) + return false; + return head.some(([type, attrs]) => type === tagType && attrs[keyAttr[0]] === keyAttr[1]); +} +function mergeHead(prev, curr) { + return [...prev.filter((tagAttrs) => !hasTag(curr, tagAttrs)), ...curr]; +} +const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g; +const DRIVE_LETTER_REGEX = /^[a-z]:/i; +function sanitizeFileName(name) { + const match = DRIVE_LETTER_REGEX.exec(name); + const driveLetter = match ? match[0] : ""; + return driveLetter + name.slice(driveLetter.length).replace(INVALID_CHAR_REGEX, "_").replace(/(^|\/)_+(?=[^/]*$)/, "$1"); +} +const KNOWN_EXTENSIONS = /* @__PURE__ */ new Set(); +function treatAsHtml(filename) { + var _a; + if (KNOWN_EXTENSIONS.size === 0) { + const extraExts = typeof process === "object" && ((_a = process.env) == null ? void 0 : _a.VITE_EXTRA_EXTENSIONS) || (__vite_import_meta_env__ == null ? void 0 : __vite_import_meta_env__.VITE_EXTRA_EXTENSIONS) || ""; + ("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip" + (extraExts && typeof extraExts === "string" ? "," + extraExts : "")).split(",").forEach((ext2) => KNOWN_EXTENSIONS.add(ext2)); + } + const ext = filename.split(".").pop(); + return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase()); +} +function escapeRegExp(str) { + return str.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d"); +} +const dataSymbol = Symbol(); +const siteDataRef = shallowRef(siteData); +function initData(route) { + const site = computed(() => resolveSiteDataByRoute(siteDataRef.value, route.data.relativePath)); + const appearance = site.value.appearance; + const isDark = appearance === "force-dark" ? ref(true) : appearance === "force-auto" ? usePreferredDark() : appearance ? useDark({ + storageKey: APPEARANCE_KEY, + initialValue: () => appearance === "dark" ? "dark" : "auto", + ...typeof appearance === "object" ? appearance : {} + }) : ref(false); + const hashRef = ref(inBrowser ? location.hash : ""); + if (inBrowser) { + window.addEventListener("hashchange", () => { + hashRef.value = location.hash; + }); + } + watch(() => route.data, () => { + hashRef.value = inBrowser ? location.hash : ""; + }); + return { + site, + theme: computed(() => site.value.themeConfig), + page: computed(() => route.data), + frontmatter: computed(() => route.data.frontmatter), + params: computed(() => route.data.params), + lang: computed(() => site.value.lang), + dir: computed(() => route.data.frontmatter.dir || site.value.dir), + localeIndex: computed(() => site.value.localeIndex || "root"), + title: computed(() => createTitle(site.value, route.data)), + description: computed(() => route.data.description || site.value.description), + isDark, + hash: computed(() => hashRef.value) + }; +} +function useData$1() { + const data = inject(dataSymbol); + if (!data) { + throw new Error("vitepress data not properly injected in app"); + } + return data; +} +function joinPath(base, path) { + return `${base}${path}`.replace(/\/+/g, "/"); +} +function withBase(path) { + return EXTERNAL_URL_RE.test(path) || !path.startsWith("/") ? path : joinPath(siteDataRef.value.base, path); +} +function pathToFile(path) { + let pagePath = path.replace(/\.html$/, ""); + pagePath = decodeURIComponent(pagePath); + pagePath = pagePath.replace(/\/$/, "/index"); + { + if (inBrowser) { + const base = "/ai-coding-kit/"; + pagePath = sanitizeFileName(pagePath.slice(base.length).replace(/\//g, "_") || "index") + ".md"; + let pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]; + if (!pageHash) { + pagePath = pagePath.endsWith("_index.md") ? pagePath.slice(0, -9) + ".md" : pagePath.slice(0, -3) + "_index.md"; + pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]; + } + if (!pageHash) + return null; + pagePath = `${base}${"assets"}/${pagePath}.${pageHash}.js`; + } else { + pagePath = `./${sanitizeFileName(pagePath.slice(1).replace(/\//g, "_"))}.md.js`; + } + } + return pagePath; +} +let contentUpdatedCallbacks = []; +function onContentUpdated(fn) { + contentUpdatedCallbacks.push(fn); + onUnmounted(() => { + contentUpdatedCallbacks = contentUpdatedCallbacks.filter((f) => f !== fn); + }); +} +function getScrollOffset() { + let scrollOffset = siteDataRef.value.scrollOffset; + let offset = 0; + let padding = 24; + if (typeof scrollOffset === "object" && "padding" in scrollOffset) { + padding = scrollOffset.padding; + scrollOffset = scrollOffset.selector; + } + if (typeof scrollOffset === "number") { + offset = scrollOffset; + } else if (typeof scrollOffset === "string") { + offset = tryOffsetSelector(scrollOffset, padding); + } else if (Array.isArray(scrollOffset)) { + for (const selector of scrollOffset) { + const res = tryOffsetSelector(selector, padding); + if (res) { + offset = res; + break; + } + } + } + return offset; +} +function tryOffsetSelector(selector, padding) { + const el = document.querySelector(selector); + if (!el) + return 0; + const bot = el.getBoundingClientRect().bottom; + if (bot < 0) + return 0; + return bot + padding; +} +const RouterSymbol = Symbol(); +const fakeHost = "http://a.com"; +const getDefaultRoute = () => ({ + path: "/", + component: null, + data: notFoundPageData +}); +function createRouter(loadPageModule, fallbackComponent) { + const route = reactive(getDefaultRoute()); + const router = { + route, + go + }; + async function go(href = inBrowser ? location.href : "/") { + var _a, _b; + href = normalizeHref(href); + if (await ((_a = router.onBeforeRouteChange) == null ? void 0 : _a.call(router, href)) === false) + return; + if (inBrowser && href !== normalizeHref(location.href)) { + history.replaceState({ scrollPosition: window.scrollY }, ""); + history.pushState({}, "", href); + } + await loadPage(href); + await ((_b = router.onAfterRouteChange ?? router.onAfterRouteChanged) == null ? void 0 : _b(href)); + } + let latestPendingPath = null; + async function loadPage(href, scrollPosition = 0, isRetry = false) { + var _a, _b; + if (await ((_a = router.onBeforePageLoad) == null ? void 0 : _a.call(router, href)) === false) + return; + const targetLoc = new URL(href, fakeHost); + const pendingPath = latestPendingPath = targetLoc.pathname; + try { + let page = await loadPageModule(pendingPath); + if (!page) { + throw new Error(`Page not found: ${pendingPath}`); + } + if (latestPendingPath === pendingPath) { + latestPendingPath = null; + const { default: comp, __pageData } = page; + if (!comp) { + throw new Error(`Invalid route component: ${comp}`); + } + await ((_b = router.onAfterPageLoad) == null ? void 0 : _b.call(router, href)); + route.path = inBrowser ? pendingPath : withBase(pendingPath); + route.component = markRaw(comp); + route.data = true ? markRaw(__pageData) : readonly(__pageData); + if (inBrowser) { + nextTick(() => { + let actualPathname = siteDataRef.value.base + __pageData.relativePath.replace(/(?:(^|\/)index)?\.md$/, "$1"); + if (!siteDataRef.value.cleanUrls && !actualPathname.endsWith("/")) { + actualPathname += ".html"; + } + if (actualPathname !== targetLoc.pathname) { + targetLoc.pathname = actualPathname; + href = actualPathname + targetLoc.search + targetLoc.hash; + history.replaceState({}, "", href); + } + if (targetLoc.hash && !scrollPosition) { + let target = null; + try { + target = document.getElementById(decodeURIComponent(targetLoc.hash).slice(1)); + } catch (e) { + console.warn(e); + } + if (target) { + scrollTo(target, targetLoc.hash); + return; + } + } + window.scrollTo(0, scrollPosition); + }); + } + } + } catch (err) { + if (!/fetch|Page not found/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) { + console.error(err); + } + if (!isRetry) { + try { + const res = await fetch(siteDataRef.value.base + "hashmap.json"); + window.__VP_HASH_MAP__ = await res.json(); + await loadPage(href, scrollPosition, true); + return; + } catch (e) { + } + } + if (latestPendingPath === pendingPath) { + latestPendingPath = null; + route.path = inBrowser ? pendingPath : withBase(pendingPath); + route.component = fallbackComponent ? markRaw(fallbackComponent) : null; + const relativePath = inBrowser ? pendingPath.replace(/(^|\/)$/, "$1index").replace(/(\.html)?$/, ".md").replace(/^\//, "") : "404.md"; + route.data = { ...notFoundPageData, relativePath }; + } + } + } + if (inBrowser) { + if (history.state === null) { + history.replaceState({}, ""); + } + window.addEventListener("click", (e) => { + if (e.defaultPrevented || !(e.target instanceof Element) || e.target.closest("button") || // temporary fix for docsearch action buttons + e.button !== 0 || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) + return; + const link2 = e.target.closest("a"); + if (!link2 || link2.closest(".vp-raw") || link2.hasAttribute("download") || link2.hasAttribute("target")) + return; + const linkHref = link2.getAttribute("href") ?? (link2 instanceof SVGAElement ? link2.getAttribute("xlink:href") : null); + if (linkHref == null) + return; + const { href, origin, pathname, hash, search } = new URL(linkHref, link2.baseURI); + const currentUrl = new URL(location.href); + if (origin === currentUrl.origin && treatAsHtml(pathname)) { + e.preventDefault(); + if (pathname === currentUrl.pathname && search === currentUrl.search) { + if (hash !== currentUrl.hash) { + history.pushState({}, "", href); + window.dispatchEvent(new HashChangeEvent("hashchange", { + oldURL: currentUrl.href, + newURL: href + })); + } + if (hash) { + scrollTo(link2, hash, link2.classList.contains("header-anchor")); + } else { + window.scrollTo(0, 0); + } + } else { + go(href); + } + } + }, { capture: true }); + window.addEventListener("popstate", async (e) => { + var _a; + if (e.state === null) + return; + const href = normalizeHref(location.href); + await loadPage(href, e.state && e.state.scrollPosition || 0); + await ((_a = router.onAfterRouteChange ?? router.onAfterRouteChanged) == null ? void 0 : _a(href)); + }); + window.addEventListener("hashchange", (e) => { + e.preventDefault(); + }); + } + return router; +} +function useRouter() { + const router = inject(RouterSymbol); + if (!router) { + throw new Error("useRouter() is called without provider."); + } + return router; +} +function useRoute() { + return useRouter().route; +} +function scrollTo(el, hash, smooth = false) { + let target = null; + try { + target = el.classList.contains("header-anchor") ? el : document.getElementById(decodeURIComponent(hash).slice(1)); + } catch (e) { + console.warn(e); + } + if (target) { + let scrollToTarget = function() { + if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight) + window.scrollTo(0, targetTop); + else + window.scrollTo({ left: 0, top: targetTop, behavior: "smooth" }); + }; + const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10); + const targetTop = window.scrollY + target.getBoundingClientRect().top - getScrollOffset() + targetPadding; + requestAnimationFrame(scrollToTarget); + } +} +function normalizeHref(href) { + const url = new URL(href, fakeHost); + url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, "$1"); + if (siteDataRef.value.cleanUrls) + url.pathname = url.pathname.replace(/\.html$/, ""); + else if (!url.pathname.endsWith("/") && !url.pathname.endsWith(".html")) + url.pathname += ".html"; + return url.pathname + url.search + url.hash; +} +const runCbs = () => contentUpdatedCallbacks.forEach((fn) => fn()); +const Content = defineComponent({ + name: "VitePressContent", + props: { + as: { type: [Object, String], default: "div" } + }, + setup(props) { + const route = useRoute(); + const { frontmatter, site } = useData$1(); + watch(frontmatter, runCbs, { deep: true, flush: "post" }); + return () => h(props.as, site.value.contentProps ?? { style: { position: "relative" } }, [ + route.component ? h(route.component, { + onVnodeMounted: runCbs, + onVnodeUpdated: runCbs, + onVnodeUnmounted: runCbs + }) : "404 Page Not Found" + ]); + } +}); +const _sfc_main$13 = /* @__PURE__ */ defineComponent({ + __name: "VPBackdrop", + __ssrInlineRender: true, + props: { + show: { type: Boolean } + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + if (__props.show) { + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$13 = _sfc_main$13.setup; +_sfc_main$13.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPBackdrop.vue"); + return _sfc_setup$13 ? _sfc_setup$13(props, ctx) : void 0; +}; +const VPBackdrop = /* @__PURE__ */ _export_sfc(_sfc_main$13, [["__scopeId", "data-v-c79a1216"]]); +const useData = useData$1; +function throttleAndDebounce(fn, delay) { + let timeoutId; + let called = false; + return () => { + if (timeoutId) + clearTimeout(timeoutId); + if (!called) { + fn(); + (called = true) && setTimeout(() => called = false, delay); + } else + timeoutId = setTimeout(fn, delay); + }; +} +function ensureStartingSlash(path) { + return path.startsWith("/") ? path : `/${path}`; +} +function normalizeLink$1(url) { + const { pathname, search, hash, protocol } = new URL(url, "http://a.com"); + if (isExternal(url) || url.startsWith("#") || !protocol.startsWith("http") || !treatAsHtml(pathname)) + return url; + const { site } = useData(); + const normalizedPath = pathname.endsWith("/") || pathname.endsWith(".html") ? url : url.replace(/(?:(^\.+)\/)?.*$/, `$1${pathname.replace(/(\.md)?$/, site.value.cleanUrls ? "" : ".html")}${search}${hash}`); + return withBase(normalizedPath); +} +function useLangs({ correspondingLink = false } = {}) { + const { site, localeIndex, page, theme: theme2, hash } = useData(); + const currentLang = computed(() => { + var _a, _b; + return { + label: (_a = site.value.locales[localeIndex.value]) == null ? void 0 : _a.label, + link: ((_b = site.value.locales[localeIndex.value]) == null ? void 0 : _b.link) || (localeIndex.value === "root" ? "/" : `/${localeIndex.value}/`) + }; + }); + const localeLinks = computed(() => Object.entries(site.value.locales).flatMap(([key, value]) => currentLang.value.label === value.label ? [] : { + text: value.label, + link: normalizeLink(value.link || (key === "root" ? "/" : `/${key}/`), theme2.value.i18nRouting !== false && correspondingLink, page.value.relativePath.slice(currentLang.value.link.length - 1), !site.value.cleanUrls) + hash.value + })); + return { localeLinks, currentLang }; +} +function normalizeLink(link2, addPath, path, addExt) { + return addPath ? link2.replace(/\/$/, "") + ensureStartingSlash(path.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, addExt ? ".html" : "")) : link2; +} +const _sfc_main$12 = /* @__PURE__ */ defineComponent({ + __name: "NotFound", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + const { currentLang } = useLangs(); + return (_ctx, _push, _parent, _attrs) => { + var _a, _b, _c, _d, _e; + _push(`

${ssrInterpolate(((_a = unref(theme2).notFound) == null ? void 0 : _a.code) ?? "404")}

${ssrInterpolate(((_b = unref(theme2).notFound) == null ? void 0 : _b.title) ?? "PAGE NOT FOUND")}

${ssrInterpolate(((_c = unref(theme2).notFound) == null ? void 0 : _c.quote) ?? "But if you don't change your direction, and if you keep looking, you may end up where you are heading.")}
`); + }; + } +}); +const _sfc_setup$12 = _sfc_main$12.setup; +_sfc_main$12.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/NotFound.vue"); + return _sfc_setup$12 ? _sfc_setup$12(props, ctx) : void 0; +}; +const NotFound = /* @__PURE__ */ _export_sfc(_sfc_main$12, [["__scopeId", "data-v-d6be1790"]]); +function getSidebar(_sidebar, path) { + if (Array.isArray(_sidebar)) + return addBase(_sidebar); + if (_sidebar == null) + return []; + path = ensureStartingSlash(path); + const dir = Object.keys(_sidebar).sort((a, b) => { + return b.split("/").length - a.split("/").length; + }).find((dir2) => { + return path.startsWith(ensureStartingSlash(dir2)); + }); + const sidebar = dir ? _sidebar[dir] : []; + return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base); +} +function getSidebarGroups(sidebar) { + const groups = []; + let lastGroupIndex = 0; + for (const index in sidebar) { + const item = sidebar[index]; + if (item.items) { + lastGroupIndex = groups.push(item); + continue; + } + if (!groups[lastGroupIndex]) { + groups.push({ items: [] }); + } + groups[lastGroupIndex].items.push(item); + } + return groups; +} +function getFlatSideBarLinks(sidebar) { + const links = []; + function recursivelyExtractLinks(items) { + for (const item of items) { + if (item.text && item.link) { + links.push({ + text: item.text, + link: item.link, + docFooterText: item.docFooterText + }); + } + if (item.items) { + recursivelyExtractLinks(item.items); + } + } + } + recursivelyExtractLinks(sidebar); + return links; +} +function hasActiveLink(path, items) { + if (Array.isArray(items)) { + return items.some((item) => hasActiveLink(path, item)); + } + return isActive(path, items.link) ? true : items.items ? hasActiveLink(path, items.items) : false; +} +function addBase(items, _base) { + return [...items].map((_item) => { + const item = { ..._item }; + const base = item.base || _base; + if (base && item.link) + item.link = base + item.link; + if (item.items) + item.items = addBase(item.items, base); + return item; + }); +} +function useSidebar() { + const { frontmatter, page, theme: theme2 } = useData(); + const is960 = useMediaQuery("(min-width: 960px)"); + const isOpen = ref(false); + const _sidebar = computed(() => { + const sidebarConfig = theme2.value.sidebar; + const relativePath = page.value.relativePath; + return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : []; + }); + const sidebar = ref(_sidebar.value); + watch(_sidebar, (next, prev) => { + if (JSON.stringify(next) !== JSON.stringify(prev)) + sidebar.value = _sidebar.value; + }); + const hasSidebar = computed(() => { + return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home"; + }); + const leftAside = computed(() => { + if (hasAside) + return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left"; + return false; + }); + const hasAside = computed(() => { + if (frontmatter.value.layout === "home") + return false; + if (frontmatter.value.aside != null) + return !!frontmatter.value.aside; + return theme2.value.aside !== false; + }); + const isSidebarEnabled = computed(() => hasSidebar.value && is960.value); + const sidebarGroups = computed(() => { + return hasSidebar.value ? getSidebarGroups(sidebar.value) : []; + }); + function open() { + isOpen.value = true; + } + function close() { + isOpen.value = false; + } + function toggle() { + isOpen.value ? close() : open(); + } + return { + isOpen, + sidebar, + sidebarGroups, + hasSidebar, + hasAside, + leftAside, + isSidebarEnabled, + open, + close, + toggle + }; +} +function useCloseSidebarOnEscape(isOpen, close) { + let triggerElement; + watchEffect(() => { + triggerElement = isOpen.value ? document.activeElement : void 0; + }); + onMounted(() => { + window.addEventListener("keyup", onEscape); + }); + onUnmounted(() => { + window.removeEventListener("keyup", onEscape); + }); + function onEscape(e) { + if (e.key === "Escape" && isOpen.value) { + close(); + triggerElement == null ? void 0 : triggerElement.focus(); + } + } +} +function useSidebarControl(item) { + const { page, hash } = useData(); + const collapsed = ref(false); + const collapsible = computed(() => { + return item.value.collapsed != null; + }); + const isLink = computed(() => { + return !!item.value.link; + }); + const isActiveLink = ref(false); + const updateIsActiveLink = () => { + isActiveLink.value = isActive(page.value.relativePath, item.value.link); + }; + watch([page, item, hash], updateIsActiveLink); + onMounted(updateIsActiveLink); + const hasActiveLink$1 = computed(() => { + if (isActiveLink.value) { + return true; + } + return item.value.items ? hasActiveLink(page.value.relativePath, item.value.items) : false; + }); + const hasChildren = computed(() => { + return !!(item.value.items && item.value.items.length); + }); + watchEffect(() => { + collapsed.value = !!(collapsible.value && item.value.collapsed); + }); + watchPostEffect(() => { + (isActiveLink.value || hasActiveLink$1.value) && (collapsed.value = false); + }); + function toggle() { + if (collapsible.value) { + collapsed.value = !collapsed.value; + } + } + return { + collapsed, + collapsible, + isLink, + isActiveLink, + hasActiveLink: hasActiveLink$1, + hasChildren, + toggle + }; +} +function useAside() { + const { hasSidebar } = useSidebar(); + const is960 = useMediaQuery("(min-width: 960px)"); + const is1280 = useMediaQuery("(min-width: 1280px)"); + const isAsideEnabled = computed(() => { + if (!is1280.value && !is960.value) { + return false; + } + return hasSidebar.value ? is1280.value : is960.value; + }); + return { + isAsideEnabled + }; +} +const ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/; +const resolvedHeaders = []; +function resolveTitle(theme2) { + return typeof theme2.outline === "object" && !Array.isArray(theme2.outline) && theme2.outline.label || theme2.outlineTitle || "On this page"; +} +function getHeaders(range) { + const headers = [ + ...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)") + ].filter((el) => el.id && el.hasChildNodes()).map((el) => { + const level = Number(el.tagName[1]); + return { + element: el, + title: serializeHeader(el), + link: "#" + el.id, + level + }; + }); + return resolveHeaders(headers, range); +} +function serializeHeader(h2) { + let ret = ""; + for (const node of h2.childNodes) { + if (node.nodeType === 1) { + if (ignoreRE.test(node.className)) + continue; + ret += node.textContent; + } else if (node.nodeType === 3) { + ret += node.textContent; + } + } + return ret.trim(); +} +function resolveHeaders(headers, range) { + if (range === false) { + return []; + } + const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2; + const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange; + return buildTree(headers, high, low); +} +function useActiveAnchor(container, marker) { + const { isAsideEnabled } = useAside(); + const onScroll = throttleAndDebounce(setActiveLink, 100); + let prevActiveLink = null; + onMounted(() => { + requestAnimationFrame(setActiveLink); + window.addEventListener("scroll", onScroll); + }); + onUpdated(() => { + activateLink(location.hash); + }); + onUnmounted(() => { + window.removeEventListener("scroll", onScroll); + }); + function setActiveLink() { + if (!isAsideEnabled.value) { + return; + } + const scrollY = window.scrollY; + const innerHeight = window.innerHeight; + const offsetHeight = document.body.offsetHeight; + const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1; + const headers = resolvedHeaders.map(({ element, link: link2 }) => ({ + link: link2, + top: getAbsoluteTop(element) + })).filter(({ top }) => !Number.isNaN(top)).sort((a, b) => a.top - b.top); + if (!headers.length) { + activateLink(null); + return; + } + if (scrollY < 1) { + activateLink(null); + return; + } + if (isBottom) { + activateLink(headers[headers.length - 1].link); + return; + } + let activeLink = null; + for (const { link: link2, top } of headers) { + if (top > scrollY + getScrollOffset() + 4) { + break; + } + activeLink = link2; + } + activateLink(activeLink); + } + function activateLink(hash) { + if (prevActiveLink) { + prevActiveLink.classList.remove("active"); + } + if (hash == null) { + prevActiveLink = null; + } else { + prevActiveLink = container.value.querySelector(`a[href="${decodeURIComponent(hash)}"]`); + } + const activeLink = prevActiveLink; + if (activeLink) { + activeLink.classList.add("active"); + marker.value.style.top = activeLink.offsetTop + 39 + "px"; + marker.value.style.opacity = "1"; + } else { + marker.value.style.top = "33px"; + marker.value.style.opacity = "0"; + } + } +} +function getAbsoluteTop(element) { + let offsetTop = 0; + while (element !== document.body) { + if (element === null) { + return NaN; + } + offsetTop += element.offsetTop; + element = element.offsetParent; + } + return offsetTop; +} +function buildTree(data, min, max) { + resolvedHeaders.length = 0; + const result = []; + const stack = []; + data.forEach((item) => { + const node = { ...item, children: [] }; + let parent = stack[stack.length - 1]; + while (parent && parent.level >= node.level) { + stack.pop(); + parent = stack[stack.length - 1]; + } + if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) { + stack.push({ level: node.level, shouldIgnore: true }); + return; + } + if (node.level > max || node.level < min) + return; + resolvedHeaders.push({ element: node.element, link: node.link }); + if (parent) + parent.children.push(node); + else + result.push(node); + stack.push(node); + }); + return result; +} +const _sfc_main$11 = /* @__PURE__ */ defineComponent({ + __name: "VPDocOutlineItem", + __ssrInlineRender: true, + props: { + headers: {}, + root: { type: Boolean } + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + const _component_VPDocOutlineItem = resolveComponent("VPDocOutlineItem", true); + _push(``); + ssrRenderList(__props.headers, ({ children, link: link2, title }) => { + _push(`
  • ${ssrInterpolate(title)}`); + if (children == null ? void 0 : children.length) { + _push(ssrRenderComponent(_component_VPDocOutlineItem, { headers: children }, null, _parent)); + } else { + _push(``); + } + _push(`
  • `); + }); + _push(``); + }; + } +}); +const _sfc_setup$11 = _sfc_main$11.setup; +_sfc_main$11.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocOutlineItem.vue"); + return _sfc_setup$11 ? _sfc_setup$11(props, ctx) : void 0; +}; +const VPDocOutlineItem = /* @__PURE__ */ _export_sfc(_sfc_main$11, [["__scopeId", "data-v-b933a997"]]); +const _sfc_main$10 = /* @__PURE__ */ defineComponent({ + __name: "VPDocAsideOutline", + __ssrInlineRender: true, + setup(__props) { + const { frontmatter, theme: theme2 } = useData(); + const headers = shallowRef([]); + onContentUpdated(() => { + headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline); + }); + const container = ref(); + const marker = ref(); + useActiveAnchor(container, marker); + return (_ctx, _push, _parent, _attrs) => { + _push(` 0 }], + ref_key: "container", + ref: container + }, _attrs))} data-v-a5bbad30>
    ${ssrInterpolate(unref(resolveTitle)(unref(theme2)))}
    `); + _push(ssrRenderComponent(VPDocOutlineItem, { + headers: headers.value, + root: true + }, null, _parent)); + _push(`
    `); + }; + } +}); +const _sfc_setup$10 = _sfc_main$10.setup; +_sfc_main$10.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocAsideOutline.vue"); + return _sfc_setup$10 ? _sfc_setup$10(props, ctx) : void 0; +}; +const VPDocAsideOutline = /* @__PURE__ */ _export_sfc(_sfc_main$10, [["__scopeId", "data-v-a5bbad30"]]); +const _sfc_main$$ = /* @__PURE__ */ defineComponent({ + __name: "VPDocAsideCarbonAds", + __ssrInlineRender: true, + props: { + carbonAds: {} + }, + setup(__props) { + const VPCarbonAds = () => null; + return (_ctx, _push, _parent, _attrs) => { + _push(``); + _push(ssrRenderComponent(unref(VPCarbonAds), { "carbon-ads": __props.carbonAds }, null, _parent)); + _push(``); + }; + } +}); +const _sfc_setup$$ = _sfc_main$$.setup; +_sfc_main$$.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue"); + return _sfc_setup$$ ? _sfc_setup$$(props, ctx) : void 0; +}; +const _sfc_main$_ = /* @__PURE__ */ defineComponent({ + __name: "VPDocAside", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderSlot(_ctx.$slots, "aside-top", {}, null, _push, _parent); + ssrRenderSlot(_ctx.$slots, "aside-outline-before", {}, null, _push, _parent); + _push(ssrRenderComponent(VPDocAsideOutline, null, null, _parent)); + ssrRenderSlot(_ctx.$slots, "aside-outline-after", {}, null, _push, _parent); + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "aside-ads-before", {}, null, _push, _parent); + if (unref(theme2).carbonAds) { + _push(ssrRenderComponent(_sfc_main$$, { + "carbon-ads": unref(theme2).carbonAds + }, null, _parent)); + } else { + _push(``); + } + ssrRenderSlot(_ctx.$slots, "aside-ads-after", {}, null, _push, _parent); + ssrRenderSlot(_ctx.$slots, "aside-bottom", {}, null, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$_ = _sfc_main$_.setup; +_sfc_main$_.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocAside.vue"); + return _sfc_setup$_ ? _sfc_setup$_(props, ctx) : void 0; +}; +const VPDocAside = /* @__PURE__ */ _export_sfc(_sfc_main$_, [["__scopeId", "data-v-3f215769"]]); +function useEditLink() { + const { theme: theme2, page } = useData(); + return computed(() => { + const { text = "Edit this page", pattern = "" } = theme2.value.editLink || {}; + let url; + if (typeof pattern === "function") { + url = pattern(page.value); + } else { + url = pattern.replace(/:path/g, page.value.filePath); + } + return { url, text }; + }); +} +function usePrevNext() { + const { page, theme: theme2, frontmatter } = useData(); + return computed(() => { + var _a, _b, _c, _d, _e, _f, _g, _h; + const sidebar = getSidebar(theme2.value.sidebar, page.value.relativePath); + const links = getFlatSideBarLinks(sidebar); + const candidates = uniqBy(links, (link2) => link2.link.replace(/[?#].*$/, "")); + const index = candidates.findIndex((link2) => { + return isActive(page.value.relativePath, link2.link); + }); + const hidePrev = ((_a = theme2.value.docFooter) == null ? void 0 : _a.prev) === false && !frontmatter.value.prev || frontmatter.value.prev === false; + const hideNext = ((_b = theme2.value.docFooter) == null ? void 0 : _b.next) === false && !frontmatter.value.next || frontmatter.value.next === false; + return { + prev: hidePrev ? void 0 : { + text: (typeof frontmatter.value.prev === "string" ? frontmatter.value.prev : typeof frontmatter.value.prev === "object" ? frontmatter.value.prev.text : void 0) ?? ((_c = candidates[index - 1]) == null ? void 0 : _c.docFooterText) ?? ((_d = candidates[index - 1]) == null ? void 0 : _d.text), + link: (typeof frontmatter.value.prev === "object" ? frontmatter.value.prev.link : void 0) ?? ((_e = candidates[index - 1]) == null ? void 0 : _e.link) + }, + next: hideNext ? void 0 : { + text: (typeof frontmatter.value.next === "string" ? frontmatter.value.next : typeof frontmatter.value.next === "object" ? frontmatter.value.next.text : void 0) ?? ((_f = candidates[index + 1]) == null ? void 0 : _f.docFooterText) ?? ((_g = candidates[index + 1]) == null ? void 0 : _g.text), + link: (typeof frontmatter.value.next === "object" ? frontmatter.value.next.link : void 0) ?? ((_h = candidates[index + 1]) == null ? void 0 : _h.link) + } + }; + }); +} +function uniqBy(array, keyFn) { + const seen = /* @__PURE__ */ new Set(); + return array.filter((item) => { + const k = keyFn(item); + return seen.has(k) ? false : seen.add(k); + }); +} +const _sfc_main$Z = /* @__PURE__ */ defineComponent({ + __name: "VPLink", + __ssrInlineRender: true, + props: { + tag: {}, + href: {}, + noIcon: { type: Boolean }, + target: {}, + rel: {} + }, + setup(__props) { + const props = __props; + const tag = computed(() => props.tag ?? (props.href ? "a" : "span")); + const isExternal2 = computed( + () => props.href && EXTERNAL_URL_RE.test(props.href) || props.target === "_blank" + ); + return (_ctx, _push, _parent, _attrs) => { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(tag.value), mergeProps({ + class: ["VPLink", { + link: __props.href, + "vp-external-link-icon": isExternal2.value, + "no-icon": __props.noIcon + }], + href: __props.href ? unref(normalizeLink$1)(__props.href) : void 0, + target: __props.target ?? (isExternal2.value ? "_blank" : void 0), + rel: __props.rel ?? (isExternal2.value ? "noreferrer" : void 0) + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "default", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "default") + ]; + } + }), + _: 3 + }), _parent); + }; + } +}); +const _sfc_setup$Z = _sfc_main$Z.setup; +_sfc_main$Z.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPLink.vue"); + return _sfc_setup$Z ? _sfc_setup$Z(props, ctx) : void 0; +}; +const _sfc_main$Y = /* @__PURE__ */ defineComponent({ + __name: "VPDocFooterLastUpdated", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2, page, lang } = useData(); + const date = computed( + () => new Date(page.value.lastUpdated) + ); + const isoDatetime = computed(() => date.value.toISOString()); + const datetime = ref(""); + onMounted(() => { + watchEffect(() => { + var _a, _b, _c; + datetime.value = new Intl.DateTimeFormat( + ((_b = (_a = theme2.value.lastUpdated) == null ? void 0 : _a.formatOptions) == null ? void 0 : _b.forceLocale) ? lang.value : void 0, + ((_c = theme2.value.lastUpdated) == null ? void 0 : _c.formatOptions) ?? { + dateStyle: "short", + timeStyle: "short" + } + ).format(date.value); + }); + }); + return (_ctx, _push, _parent, _attrs) => { + var _a; + _push(`${ssrInterpolate(((_a = unref(theme2).lastUpdated) == null ? void 0 : _a.text) || unref(theme2).lastUpdatedText || "Last updated")}: ${ssrInterpolate(datetime.value)}

    `); + }; + } +}); +const _sfc_setup$Y = _sfc_main$Y.setup; +_sfc_main$Y.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocFooterLastUpdated.vue"); + return _sfc_setup$Y ? _sfc_setup$Y(props, ctx) : void 0; +}; +const VPDocFooterLastUpdated = /* @__PURE__ */ _export_sfc(_sfc_main$Y, [["__scopeId", "data-v-e98dd255"]]); +const _sfc_main$X = /* @__PURE__ */ defineComponent({ + __name: "VPDocFooter", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2, page, frontmatter } = useData(); + const editLink = useEditLink(); + const control = usePrevNext(); + const hasEditLink = computed( + () => theme2.value.editLink && frontmatter.value.editLink !== false + ); + const hasLastUpdated = computed(() => page.value.lastUpdated); + const showFooter = computed( + () => hasEditLink.value || hasLastUpdated.value || control.value.prev || control.value.next + ); + return (_ctx, _push, _parent, _attrs) => { + var _a, _b, _c, _d; + if (showFooter.value) { + _push(``); + ssrRenderSlot(_ctx.$slots, "doc-footer-before", {}, null, _push, _parent); + if (hasEditLink.value || hasLastUpdated.value) { + _push(`
    `); + if (hasEditLink.value) { + _push(``); + } else { + _push(``); + } + if (hasLastUpdated.value) { + _push(`
    `); + _push(ssrRenderComponent(VPDocFooterLastUpdated, null, null, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(`
    `); + } else { + _push(``); + } + if (((_a = unref(control).prev) == null ? void 0 : _a.link) || ((_b = unref(control).next) == null ? void 0 : _b.link)) { + _push(``); + } else { + _push(``); + } + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$X = _sfc_main$X.setup; +_sfc_main$X.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocFooter.vue"); + return _sfc_setup$X ? _sfc_setup$X(props, ctx) : void 0; +}; +const VPDocFooter = /* @__PURE__ */ _export_sfc(_sfc_main$X, [["__scopeId", "data-v-e257564d"]]); +const _sfc_main$W = /* @__PURE__ */ defineComponent({ + __name: "VPDoc", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + const route = useRoute(); + const { hasSidebar, hasAside, leftAside } = useSidebar(); + const pageName = computed( + () => route.path.replace(/[./]+/g, "_").replace(/_html$/, "") + ); + return (_ctx, _push, _parent, _attrs) => { + const _component_Content = resolveComponent("Content"); + _push(``); + ssrRenderSlot(_ctx.$slots, "doc-top", {}, null, _push, _parent); + _push(`
    `); + if (unref(hasAside)) { + _push(`
    `); + _push(ssrRenderComponent(VPDocAside, null, { + "aside-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-top", {}, void 0, true) + ]; + } + }), + "aside-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-bottom", {}, void 0, true) + ]; + } + }), + "aside-outline-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-before", {}, void 0, true) + ]; + } + }), + "aside-outline-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-after", {}, void 0, true) + ]; + } + }), + "aside-ads-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-before", {}, void 0, true) + ]; + } + }), + "aside-ads-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "doc-before", {}, null, _push, _parent); + _push(`
    `); + _push(ssrRenderComponent(_component_Content, { + class: ["vp-doc", [ + pageName.value, + unref(theme2).externalLinkIcon && "external-link-icon-enabled" + ]] + }, null, _parent)); + _push(`
    `); + _push(ssrRenderComponent(VPDocFooter, null, { + "doc-footer-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-footer-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-footer-before", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + ssrRenderSlot(_ctx.$slots, "doc-after", {}, null, _push, _parent); + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "doc-bottom", {}, null, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$W = _sfc_main$W.setup; +_sfc_main$W.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDoc.vue"); + return _sfc_setup$W ? _sfc_setup$W(props, ctx) : void 0; +}; +const VPDoc = /* @__PURE__ */ _export_sfc(_sfc_main$W, [["__scopeId", "data-v-39a288b8"]]); +const _sfc_main$V = /* @__PURE__ */ defineComponent({ + __name: "VPButton", + __ssrInlineRender: true, + props: { + tag: {}, + size: { default: "medium" }, + theme: { default: "brand" }, + text: {}, + href: {}, + target: {}, + rel: {} + }, + setup(__props) { + const props = __props; + const isExternal2 = computed( + () => props.href && EXTERNAL_URL_RE.test(props.href) + ); + const component = computed(() => { + return props.tag || (props.href ? "a" : "button"); + }); + return (_ctx, _push, _parent, _attrs) => { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(component.value), mergeProps({ + class: ["VPButton", [__props.size, __props.theme]], + href: __props.href ? unref(normalizeLink$1)(__props.href) : void 0, + target: props.target ?? (isExternal2.value ? "_blank" : void 0), + rel: props.rel ?? (isExternal2.value ? "noreferrer" : void 0) + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${ssrInterpolate(__props.text)}`); + } else { + return [ + createTextVNode(toDisplayString(__props.text), 1) + ]; + } + }), + _: 1 + }), _parent); + }; + } +}); +const _sfc_setup$V = _sfc_main$V.setup; +_sfc_main$V.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPButton.vue"); + return _sfc_setup$V ? _sfc_setup$V(props, ctx) : void 0; +}; +const VPButton = /* @__PURE__ */ _export_sfc(_sfc_main$V, [["__scopeId", "data-v-fa7799d5"]]); +const _sfc_main$U = /* @__PURE__ */ defineComponent({ + ...{ inheritAttrs: false }, + __name: "VPImage", + __ssrInlineRender: true, + props: { + image: {}, + alt: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + const _component_VPImage = resolveComponent("VPImage", true); + if (__props.image) { + _push(``); + if (typeof __props.image === "string" || "src" in __props.image) { + _push(``); + } else { + _push(``); + _push(ssrRenderComponent(_component_VPImage, mergeProps({ + class: "dark", + image: __props.image.dark, + alt: __props.image.alt + }, _ctx.$attrs), null, _parent)); + _push(ssrRenderComponent(_component_VPImage, mergeProps({ + class: "light", + image: __props.image.light, + alt: __props.image.alt + }, _ctx.$attrs), null, _parent)); + _push(``); + } + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$U = _sfc_main$U.setup; +_sfc_main$U.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPImage.vue"); + return _sfc_setup$U ? _sfc_setup$U(props, ctx) : void 0; +}; +const VPImage = /* @__PURE__ */ _export_sfc(_sfc_main$U, [["__scopeId", "data-v-8426fc1a"]]); +const _sfc_main$T = /* @__PURE__ */ defineComponent({ + __name: "VPHero", + __ssrInlineRender: true, + props: { + name: {}, + text: {}, + tagline: {}, + image: {}, + actions: {} + }, + setup(__props) { + const heroImageSlotExists = inject("hero-image-slot-exists"); + return (_ctx, _push, _parent, _attrs) => { + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "home-hero-info-before", {}, null, _push, _parent); + ssrRenderSlot(_ctx.$slots, "home-hero-info", {}, () => { + _push(`

    `); + if (__props.name) { + _push(`${__props.name ?? ""}`); + } else { + _push(``); + } + if (__props.text) { + _push(`${__props.text ?? ""}`); + } else { + _push(``); + } + _push(`

    `); + if (__props.tagline) { + _push(`

    ${__props.tagline ?? ""}

    `); + } else { + _push(``); + } + }, _push, _parent); + ssrRenderSlot(_ctx.$slots, "home-hero-info-after", {}, null, _push, _parent); + if (__props.actions) { + _push(`
    `); + ssrRenderList(__props.actions, (action) => { + _push(`
    `); + _push(ssrRenderComponent(VPButton, { + tag: "a", + size: "medium", + theme: action.theme, + text: action.text, + href: action.link, + target: action.target, + rel: action.rel + }, null, _parent)); + _push(`
    `); + }); + _push(`
    `); + } else { + _push(``); + } + ssrRenderSlot(_ctx.$slots, "home-hero-actions-after", {}, null, _push, _parent); + _push(`
    `); + if (__props.image || unref(heroImageSlotExists)) { + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "home-hero-image", {}, () => { + if (__props.image) { + _push(ssrRenderComponent(VPImage, { + class: "image-src", + image: __props.image + }, null, _parent)); + } else { + _push(``); + } + }, _push, _parent); + _push(`
    `); + } else { + _push(``); + } + _push(`
    `); + }; + } +}); +const _sfc_setup$T = _sfc_main$T.setup; +_sfc_main$T.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHero.vue"); + return _sfc_setup$T ? _sfc_setup$T(props, ctx) : void 0; +}; +const VPHero = /* @__PURE__ */ _export_sfc(_sfc_main$T, [["__scopeId", "data-v-4f9c455b"]]); +const _sfc_main$S = /* @__PURE__ */ defineComponent({ + __name: "VPHomeHero", + __ssrInlineRender: true, + setup(__props) { + const { frontmatter: fm } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(fm).hero) { + _push(ssrRenderComponent(VPHero, mergeProps({ + class: "VPHomeHero", + name: unref(fm).hero.name, + text: unref(fm).hero.text, + tagline: unref(fm).hero.tagline, + image: unref(fm).hero.image, + actions: unref(fm).hero.actions + }, _attrs), { + "home-hero-info-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-before") + ]; + } + }), + "home-hero-info": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info") + ]; + } + }), + "home-hero-info-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-after") + ]; + } + }), + "home-hero-actions-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-actions-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-actions-after") + ]; + } + }), + "home-hero-image": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-image", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-image") + ]; + } + }), + _: 3 + }, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$S = _sfc_main$S.setup; +_sfc_main$S.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue"); + return _sfc_setup$S ? _sfc_setup$S(props, ctx) : void 0; +}; +const _sfc_main$R = /* @__PURE__ */ defineComponent({ + __name: "VPFeature", + __ssrInlineRender: true, + props: { + icon: {}, + title: {}, + details: {}, + link: {}, + linkText: {}, + rel: {}, + target: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(_sfc_main$Z, mergeProps({ + class: "VPFeature", + href: __props.link, + rel: __props.rel, + target: __props.target, + "no-icon": true, + tag: __props.link ? "a" : "div" + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`
    `); + if (typeof __props.icon === "object" && __props.icon.wrap) { + _push2(`
    `); + _push2(ssrRenderComponent(VPImage, { + image: __props.icon, + alt: __props.icon.alt, + height: __props.icon.height || 48, + width: __props.icon.width || 48 + }, null, _parent2, _scopeId)); + _push2(`
    `); + } else if (typeof __props.icon === "object") { + _push2(ssrRenderComponent(VPImage, { + image: __props.icon, + alt: __props.icon.alt, + height: __props.icon.height || 48, + width: __props.icon.width || 48 + }, null, _parent2, _scopeId)); + } else if (__props.icon) { + _push2(`
    ${__props.icon ?? ""}
    `); + } else { + _push2(``); + } + _push2(`

    ${__props.title ?? ""}

    `); + if (__props.details) { + _push2(`

    ${__props.details ?? ""}

    `); + } else { + _push2(``); + } + if (__props.linkText) { + _push2(``); + } else { + _push2(``); + } + _push2(`
    `); + } else { + return [ + createVNode("article", { class: "box" }, [ + typeof __props.icon === "object" && __props.icon.wrap ? (openBlock(), createBlock("div", { + key: 0, + class: "icon" + }, [ + createVNode(VPImage, { + image: __props.icon, + alt: __props.icon.alt, + height: __props.icon.height || 48, + width: __props.icon.width || 48 + }, null, 8, ["image", "alt", "height", "width"]) + ])) : typeof __props.icon === "object" ? (openBlock(), createBlock(VPImage, { + key: 1, + image: __props.icon, + alt: __props.icon.alt, + height: __props.icon.height || 48, + width: __props.icon.width || 48 + }, null, 8, ["image", "alt", "height", "width"])) : __props.icon ? (openBlock(), createBlock("div", { + key: 2, + class: "icon", + innerHTML: __props.icon + }, null, 8, ["innerHTML"])) : createCommentVNode("", true), + createVNode("h2", { + class: "title", + innerHTML: __props.title + }, null, 8, ["innerHTML"]), + __props.details ? (openBlock(), createBlock("p", { + key: 3, + class: "details", + innerHTML: __props.details + }, null, 8, ["innerHTML"])) : createCommentVNode("", true), + __props.linkText ? (openBlock(), createBlock("div", { + key: 4, + class: "link-text" + }, [ + createVNode("p", { class: "link-text-value" }, [ + createTextVNode(toDisplayString(__props.linkText) + " ", 1), + createVNode("span", { class: "vpi-arrow-right link-text-icon" }) + ]) + ])) : createCommentVNode("", true) + ]) + ]; + } + }), + _: 1 + }, _parent)); + }; + } +}); +const _sfc_setup$R = _sfc_main$R.setup; +_sfc_main$R.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPFeature.vue"); + return _sfc_setup$R ? _sfc_setup$R(props, ctx) : void 0; +}; +const VPFeature = /* @__PURE__ */ _export_sfc(_sfc_main$R, [["__scopeId", "data-v-a3976bdc"]]); +const _sfc_main$Q = /* @__PURE__ */ defineComponent({ + __name: "VPFeatures", + __ssrInlineRender: true, + props: { + features: {} + }, + setup(__props) { + const props = __props; + const grid = computed(() => { + const length = props.features.length; + if (!length) { + return; + } else if (length === 2) { + return "grid-2"; + } else if (length === 3) { + return "grid-3"; + } else if (length % 3 === 0) { + return "grid-6"; + } else if (length > 3) { + return "grid-4"; + } + }); + return (_ctx, _push, _parent, _attrs) => { + if (__props.features) { + _push(`
    `); + ssrRenderList(__props.features, (feature) => { + _push(`
    `); + _push(ssrRenderComponent(VPFeature, { + icon: feature.icon, + title: feature.title, + details: feature.details, + link: feature.link, + "link-text": feature.linkText, + rel: feature.rel, + target: feature.target + }, null, _parent)); + _push(`
    `); + }); + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$Q = _sfc_main$Q.setup; +_sfc_main$Q.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue"); + return _sfc_setup$Q ? _sfc_setup$Q(props, ctx) : void 0; +}; +const VPFeatures = /* @__PURE__ */ _export_sfc(_sfc_main$Q, [["__scopeId", "data-v-a6181336"]]); +const _sfc_main$P = /* @__PURE__ */ defineComponent({ + __name: "VPHomeFeatures", + __ssrInlineRender: true, + setup(__props) { + const { frontmatter: fm } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(fm).features) { + _push(ssrRenderComponent(VPFeatures, mergeProps({ + class: "VPHomeFeatures", + features: unref(fm).features + }, _attrs), null, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$P = _sfc_main$P.setup; +_sfc_main$P.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue"); + return _sfc_setup$P ? _sfc_setup$P(props, ctx) : void 0; +}; +const _sfc_main$O = /* @__PURE__ */ defineComponent({ + __name: "VPHomeContent", + __ssrInlineRender: true, + setup(__props) { + const { width: vw } = useWindowSize({ + initialWidth: 0, + includeScrollbar: false + }); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$O = _sfc_main$O.setup; +_sfc_main$O.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue"); + return _sfc_setup$O ? _sfc_setup$O(props, ctx) : void 0; +}; +const VPHomeContent = /* @__PURE__ */ _export_sfc(_sfc_main$O, [["__scopeId", "data-v-8e2d4988"]]); +const _sfc_main$N = /* @__PURE__ */ defineComponent({ + __name: "VPHome", + __ssrInlineRender: true, + setup(__props) { + const { frontmatter, theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + const _component_Content = resolveComponent("Content"); + _push(``); + ssrRenderSlot(_ctx.$slots, "home-hero-before", {}, null, _push, _parent); + _push(ssrRenderComponent(_sfc_main$S, null, { + "home-hero-info-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-before", {}, void 0, true) + ]; + } + }), + "home-hero-info": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info", {}, void 0, true) + ]; + } + }), + "home-hero-info-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-after", {}, void 0, true) + ]; + } + }), + "home-hero-actions-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-actions-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-actions-after", {}, void 0, true) + ]; + } + }), + "home-hero-image": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-image", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-image", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + ssrRenderSlot(_ctx.$slots, "home-hero-after", {}, null, _push, _parent); + ssrRenderSlot(_ctx.$slots, "home-features-before", {}, null, _push, _parent); + _push(ssrRenderComponent(_sfc_main$P, null, null, _parent)); + ssrRenderSlot(_ctx.$slots, "home-features-after", {}, null, _push, _parent); + if (unref(frontmatter).markdownStyles !== false) { + _push(ssrRenderComponent(VPHomeContent, null, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(ssrRenderComponent(_component_Content, null, null, _parent2, _scopeId)); + } else { + return [ + createVNode(_component_Content) + ]; + } + }), + _: 1 + }, _parent)); + } else { + _push(ssrRenderComponent(_component_Content, null, null, _parent)); + } + _push(``); + }; + } +}); +const _sfc_setup$N = _sfc_main$N.setup; +_sfc_main$N.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHome.vue"); + return _sfc_setup$N ? _sfc_setup$N(props, ctx) : void 0; +}; +const VPHome = /* @__PURE__ */ _export_sfc(_sfc_main$N, [["__scopeId", "data-v-8b561e3d"]]); +const _sfc_main$M = {}; +function _sfc_ssrRender$1(_ctx, _push, _parent, _attrs) { + const _component_Content = resolveComponent("Content"); + _push(``); + ssrRenderSlot(_ctx.$slots, "page-top", {}, null, _push, _parent); + _push(ssrRenderComponent(_component_Content, null, null, _parent)); + ssrRenderSlot(_ctx.$slots, "page-bottom", {}, null, _push, _parent); + _push(``); +} +const _sfc_setup$M = _sfc_main$M.setup; +_sfc_main$M.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPPage.vue"); + return _sfc_setup$M ? _sfc_setup$M(props, ctx) : void 0; +}; +const VPPage = /* @__PURE__ */ _export_sfc(_sfc_main$M, [["ssrRender", _sfc_ssrRender$1]]); +const _sfc_main$L = /* @__PURE__ */ defineComponent({ + __name: "VPContent", + __ssrInlineRender: true, + setup(__props) { + const { page, frontmatter } = useData(); + const { hasSidebar } = useSidebar(); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + if (unref(page).isNotFound) { + ssrRenderSlot(_ctx.$slots, "not-found", {}, () => { + _push(ssrRenderComponent(NotFound, null, null, _parent)); + }, _push, _parent); + } else if (unref(frontmatter).layout === "page") { + _push(ssrRenderComponent(VPPage, null, { + "page-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "page-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "page-top", {}, void 0, true) + ]; + } + }), + "page-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "page-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "page-bottom", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + } else if (unref(frontmatter).layout === "home") { + _push(ssrRenderComponent(VPHome, null, { + "home-hero-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-before", {}, void 0, true) + ]; + } + }), + "home-hero-info-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-before", {}, void 0, true) + ]; + } + }), + "home-hero-info": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info", {}, void 0, true) + ]; + } + }), + "home-hero-info-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-after", {}, void 0, true) + ]; + } + }), + "home-hero-actions-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-actions-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-actions-after", {}, void 0, true) + ]; + } + }), + "home-hero-image": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-image", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-image", {}, void 0, true) + ]; + } + }), + "home-hero-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-after", {}, void 0, true) + ]; + } + }), + "home-features-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-features-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-features-before", {}, void 0, true) + ]; + } + }), + "home-features-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-features-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-features-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + } else if (unref(frontmatter).layout && unref(frontmatter).layout !== "doc") { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(unref(frontmatter).layout), null, null), _parent); + } else { + _push(ssrRenderComponent(VPDoc, null, { + "doc-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-top", {}, void 0, true) + ]; + } + }), + "doc-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-bottom", {}, void 0, true) + ]; + } + }), + "doc-footer-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-footer-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-footer-before", {}, void 0, true) + ]; + } + }), + "doc-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-before", {}, void 0, true) + ]; + } + }), + "doc-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-after", {}, void 0, true) + ]; + } + }), + "aside-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-top", {}, void 0, true) + ]; + } + }), + "aside-outline-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-before", {}, void 0, true) + ]; + } + }), + "aside-outline-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-after", {}, void 0, true) + ]; + } + }), + "aside-ads-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-before", {}, void 0, true) + ]; + } + }), + "aside-ads-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-after", {}, void 0, true) + ]; + } + }), + "aside-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-bottom", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + } + _push(``); + }; + } +}); +const _sfc_setup$L = _sfc_main$L.setup; +_sfc_main$L.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPContent.vue"); + return _sfc_setup$L ? _sfc_setup$L(props, ctx) : void 0; +}; +const VPContent = /* @__PURE__ */ _export_sfc(_sfc_main$L, [["__scopeId", "data-v-1428d186"]]); +const _sfc_main$K = /* @__PURE__ */ defineComponent({ + __name: "VPFooter", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2, frontmatter } = useData(); + const { hasSidebar } = useSidebar(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(theme2).footer && unref(frontmatter).footer !== false) { + _push(`
    `); + if (unref(theme2).footer.message) { + _push(`

    ${unref(theme2).footer.message ?? ""}

    `); + } else { + _push(``); + } + if (unref(theme2).footer.copyright) { + _push(``); + } else { + _push(``); + } + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$K = _sfc_main$K.setup; +_sfc_main$K.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPFooter.vue"); + return _sfc_setup$K ? _sfc_setup$K(props, ctx) : void 0; +}; +const VPFooter = /* @__PURE__ */ _export_sfc(_sfc_main$K, [["__scopeId", "data-v-e315a0ad"]]); +function useLocalNav() { + const { theme: theme2, frontmatter } = useData(); + const headers = shallowRef([]); + const hasLocalNav = computed(() => { + return headers.value.length > 0; + }); + onContentUpdated(() => { + headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline); + }); + return { + headers, + hasLocalNav + }; +} +const _sfc_main$J = /* @__PURE__ */ defineComponent({ + __name: "VPLocalNavOutlineDropdown", + __ssrInlineRender: true, + props: { + headers: {}, + navHeight: {} + }, + setup(__props) { + const { theme: theme2 } = useData(); + const open = ref(false); + const vh = ref(0); + const main = ref(); + ref(); + function closeOnClickOutside(e) { + var _a; + if (!((_a = main.value) == null ? void 0 : _a.contains(e.target))) { + open.value = false; + } + } + watch(open, (value) => { + if (value) { + document.addEventListener("click", closeOnClickOutside); + return; + } + document.removeEventListener("click", closeOnClickOutside); + }); + onKeyStroke("Escape", () => { + open.value = false; + }); + onContentUpdated(() => { + open.value = false; + }); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + if (__props.headers.length > 0) { + _push(``); + } else { + _push(``); + } + if (open.value) { + _push(`
    `); + _push(ssrRenderComponent(VPDocOutlineItem, { headers: __props.headers }, null, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(``); + }; + } +}); +const _sfc_setup$J = _sfc_main$J.setup; +_sfc_main$J.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPLocalNavOutlineDropdown.vue"); + return _sfc_setup$J ? _sfc_setup$J(props, ctx) : void 0; +}; +const VPLocalNavOutlineDropdown = /* @__PURE__ */ _export_sfc(_sfc_main$J, [["__scopeId", "data-v-8a42e2b4"]]); +const _sfc_main$I = /* @__PURE__ */ defineComponent({ + __name: "VPLocalNav", + __ssrInlineRender: true, + props: { + open: { type: Boolean } + }, + emits: ["open-menu"], + setup(__props) { + const { theme: theme2, frontmatter } = useData(); + const { hasSidebar } = useSidebar(); + const { headers } = useLocalNav(); + const { y } = useWindowScroll(); + const navHeight = ref(0); + onMounted(() => { + navHeight.value = parseInt( + getComputedStyle(document.documentElement).getPropertyValue( + "--vp-nav-height" + ) + ); + }); + onContentUpdated(() => { + headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline); + }); + const empty = computed(() => { + return headers.value.length === 0; + }); + const emptyAndNoSidebar = computed(() => { + return empty.value && !hasSidebar.value; + }); + const classes = computed(() => { + return { + VPLocalNav: true, + "has-sidebar": hasSidebar.value, + empty: empty.value, + fixed: emptyAndNoSidebar.value + }; + }); + return (_ctx, _push, _parent, _attrs) => { + if (unref(frontmatter).layout !== "home" && (!emptyAndNoSidebar.value || unref(y) >= navHeight.value)) { + _push(`
    `); + if (unref(hasSidebar)) { + _push(``); + } else { + _push(``); + } + _push(ssrRenderComponent(VPLocalNavOutlineDropdown, { + headers: unref(headers), + navHeight: navHeight.value + }, null, _parent)); + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$I = _sfc_main$I.setup; +_sfc_main$I.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPLocalNav.vue"); + return _sfc_setup$I ? _sfc_setup$I(props, ctx) : void 0; +}; +const VPLocalNav = /* @__PURE__ */ _export_sfc(_sfc_main$I, [["__scopeId", "data-v-a6f0e41e"]]); +function useNav() { + const isScreenOpen = ref(false); + function openScreen() { + isScreenOpen.value = true; + window.addEventListener("resize", closeScreenOnTabletWindow); + } + function closeScreen() { + isScreenOpen.value = false; + window.removeEventListener("resize", closeScreenOnTabletWindow); + } + function toggleScreen() { + isScreenOpen.value ? closeScreen() : openScreen(); + } + function closeScreenOnTabletWindow() { + window.outerWidth >= 768 && closeScreen(); + } + const route = useRoute(); + watch(() => route.path, closeScreen); + return { + isScreenOpen, + openScreen, + closeScreen, + toggleScreen + }; +} +const _sfc_main$H = {}; +function _sfc_ssrRender(_ctx, _push, _parent, _attrs) { + _push(``); + if (_ctx.$slots.default) { + _push(``); + ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent); + _push(``); + } else { + _push(``); + } + _push(``); +} +const _sfc_setup$H = _sfc_main$H.setup; +_sfc_main$H.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSwitch.vue"); + return _sfc_setup$H ? _sfc_setup$H(props, ctx) : void 0; +}; +const VPSwitch = /* @__PURE__ */ _export_sfc(_sfc_main$H, [["ssrRender", _sfc_ssrRender], ["__scopeId", "data-v-1d5665e3"]]); +const _sfc_main$G = /* @__PURE__ */ defineComponent({ + __name: "VPSwitchAppearance", + __ssrInlineRender: true, + setup(__props) { + const { isDark, theme: theme2 } = useData(); + const toggleAppearance = inject("toggle-appearance", () => { + isDark.value = !isDark.value; + }); + const switchTitle = ref(""); + watchPostEffect(() => { + switchTitle.value = isDark.value ? theme2.value.lightModeSwitchTitle || "Switch to light theme" : theme2.value.darkModeSwitchTitle || "Switch to dark theme"; + }); + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(VPSwitch, mergeProps({ + title: switchTitle.value, + class: "VPSwitchAppearance", + "aria-checked": unref(isDark), + onClick: unref(toggleAppearance) + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(``); + } else { + return [ + createVNode("span", { class: "vpi-sun sun" }), + createVNode("span", { class: "vpi-moon moon" }) + ]; + } + }), + _: 1 + }, _parent)); + }; + } +}); +const _sfc_setup$G = _sfc_main$G.setup; +_sfc_main$G.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSwitchAppearance.vue"); + return _sfc_setup$G ? _sfc_setup$G(props, ctx) : void 0; +}; +const VPSwitchAppearance = /* @__PURE__ */ _export_sfc(_sfc_main$G, [["__scopeId", "data-v-5337faa4"]]); +const _sfc_main$F = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarAppearance", + __ssrInlineRender: true, + setup(__props) { + const { site } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(site).appearance && unref(site).appearance !== "force-dark" && unref(site).appearance !== "force-auto") { + _push(``); + _push(ssrRenderComponent(VPSwitchAppearance, null, null, _parent)); + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$F = _sfc_main$F.setup; +_sfc_main$F.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue"); + return _sfc_setup$F ? _sfc_setup$F(props, ctx) : void 0; +}; +const VPNavBarAppearance = /* @__PURE__ */ _export_sfc(_sfc_main$F, [["__scopeId", "data-v-6c893767"]]); +const focusedElement = ref(); +let active = false; +let listeners = 0; +function useFlyout(options) { + const focus = ref(false); + if (inBrowser) { + !active && activateFocusTracking(); + listeners++; + const unwatch = watch(focusedElement, (el) => { + var _a, _b, _c; + if (el === options.el.value || ((_a = options.el.value) == null ? void 0 : _a.contains(el))) { + focus.value = true; + (_b = options.onFocus) == null ? void 0 : _b.call(options); + } else { + focus.value = false; + (_c = options.onBlur) == null ? void 0 : _c.call(options); + } + }); + onUnmounted(() => { + unwatch(); + listeners--; + if (!listeners) { + deactivateFocusTracking(); + } + }); + } + return readonly(focus); +} +function activateFocusTracking() { + document.addEventListener("focusin", handleFocusIn); + active = true; + focusedElement.value = document.activeElement; +} +function deactivateFocusTracking() { + document.removeEventListener("focusin", handleFocusIn); +} +function handleFocusIn() { + focusedElement.value = document.activeElement; +} +const _sfc_main$E = /* @__PURE__ */ defineComponent({ + __name: "VPMenuLink", + __ssrInlineRender: true, + props: { + item: {} + }, + setup(__props) { + const { page } = useData(); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + _push(ssrRenderComponent(_sfc_main$Z, { + class: { + active: unref(isActive)( + unref(page).relativePath, + __props.item.activeMatch || __props.item.link, + !!__props.item.activeMatch + ) + }, + href: __props.item.link, + target: __props.item.target, + rel: __props.item.rel, + "no-icon": __props.item.noIcon + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${__props.item.text ?? ""}`); + } else { + return [ + createVNode("span", { + innerHTML: __props.item.text + }, null, 8, ["innerHTML"]) + ]; + } + }), + _: 1 + }, _parent)); + _push(``); + }; + } +}); +const _sfc_setup$E = _sfc_main$E.setup; +_sfc_main$E.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPMenuLink.vue"); + return _sfc_setup$E ? _sfc_setup$E(props, ctx) : void 0; +}; +const VPMenuLink = /* @__PURE__ */ _export_sfc(_sfc_main$E, [["__scopeId", "data-v-35975db6"]]); +const _sfc_main$D = /* @__PURE__ */ defineComponent({ + __name: "VPMenuGroup", + __ssrInlineRender: true, + props: { + text: {}, + items: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + if (__props.text) { + _push(`

    ${ssrInterpolate(__props.text)}

    `); + } else { + _push(``); + } + _push(``); + ssrRenderList(__props.items, (item) => { + _push(``); + if ("link" in item) { + _push(ssrRenderComponent(VPMenuLink, { item }, null, _parent)); + } else { + _push(``); + } + _push(``); + }); + _push(``); + }; + } +}); +const _sfc_setup$D = _sfc_main$D.setup; +_sfc_main$D.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPMenuGroup.vue"); + return _sfc_setup$D ? _sfc_setup$D(props, ctx) : void 0; +}; +const VPMenuGroup = /* @__PURE__ */ _export_sfc(_sfc_main$D, [["__scopeId", "data-v-69e747b5"]]); +const _sfc_main$C = /* @__PURE__ */ defineComponent({ + __name: "VPMenu", + __ssrInlineRender: true, + props: { + items: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + if (__props.items) { + _push(`
    `); + ssrRenderList(__props.items, (item) => { + _push(``); + if ("link" in item) { + _push(ssrRenderComponent(VPMenuLink, { item }, null, _parent)); + } else if ("component" in item) { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(item.component), mergeProps({ ref_for: true }, item.props), null), _parent); + } else { + _push(ssrRenderComponent(VPMenuGroup, { + text: item.text, + items: item.items + }, null, _parent)); + } + _push(``); + }); + _push(`
    `); + } else { + _push(``); + } + ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$C = _sfc_main$C.setup; +_sfc_main$C.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPMenu.vue"); + return _sfc_setup$C ? _sfc_setup$C(props, ctx) : void 0; +}; +const VPMenu = /* @__PURE__ */ _export_sfc(_sfc_main$C, [["__scopeId", "data-v-b98bc113"]]); +const _sfc_main$B = /* @__PURE__ */ defineComponent({ + __name: "VPFlyout", + __ssrInlineRender: true, + props: { + icon: {}, + button: {}, + label: {}, + items: {} + }, + setup(__props) { + const open = ref(false); + const el = ref(); + useFlyout({ el, onBlur }); + function onBlur() { + open.value = false; + } + return (_ctx, _push, _parent, _attrs) => { + _push(``); + }; + } +}); +const _sfc_setup$B = _sfc_main$B.setup; +_sfc_main$B.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPFlyout.vue"); + return _sfc_setup$B ? _sfc_setup$B(props, ctx) : void 0; +}; +const VPFlyout = /* @__PURE__ */ _export_sfc(_sfc_main$B, [["__scopeId", "data-v-cf11d7a2"]]); +const _sfc_main$A = /* @__PURE__ */ defineComponent({ + __name: "VPSocialLink", + __ssrInlineRender: true, + props: { + icon: {}, + link: {}, + ariaLabel: {} + }, + setup(__props) { + var _a; + const props = __props; + const el = ref(); + onMounted(async () => { + var _a2; + await nextTick(); + const span = (_a2 = el.value) == null ? void 0 : _a2.children[0]; + if (span instanceof HTMLElement && span.className.startsWith("vpi-social-") && (getComputedStyle(span).maskImage || getComputedStyle(span).webkitMaskImage) === "none") { + span.style.setProperty( + "--icon", + `url('https://api.iconify.design/simple-icons/${props.icon}.svg')` + ); + } + }); + const svg = computed(() => { + if (typeof props.icon === "object") return props.icon.svg; + return ``; + }); + { + typeof props.icon === "string" && ((_a = useSSRContext()) == null ? void 0 : _a.vpSocialIcons.add(props.icon)); + } + return (_ctx, _push, _parent, _attrs) => { + _push(`${svg.value ?? ""}`); + }; + } +}); +const _sfc_setup$A = _sfc_main$A.setup; +_sfc_main$A.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue"); + return _sfc_setup$A ? _sfc_setup$A(props, ctx) : void 0; +}; +const VPSocialLink = /* @__PURE__ */ _export_sfc(_sfc_main$A, [["__scopeId", "data-v-bd121fe5"]]); +const _sfc_main$z = /* @__PURE__ */ defineComponent({ + __name: "VPSocialLinks", + __ssrInlineRender: true, + props: { + links: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderList(__props.links, ({ link: link2, icon, ariaLabel }) => { + _push(ssrRenderComponent(VPSocialLink, { + key: link2, + icon, + link: link2, + ariaLabel + }, null, _parent)); + }); + _push(``); + }; + } +}); +const _sfc_setup$z = _sfc_main$z.setup; +_sfc_main$z.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue"); + return _sfc_setup$z ? _sfc_setup$z(props, ctx) : void 0; +}; +const VPSocialLinks = /* @__PURE__ */ _export_sfc(_sfc_main$z, [["__scopeId", "data-v-7bc22406"]]); +const _sfc_main$y = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarExtra", + __ssrInlineRender: true, + setup(__props) { + const { site, theme: theme2 } = useData(); + const { localeLinks, currentLang } = useLangs({ correspondingLink: true }); + const hasExtraContent = computed( + () => localeLinks.value.length && currentLang.value.label || site.value.appearance || theme2.value.socialLinks + ); + return (_ctx, _push, _parent, _attrs) => { + if (hasExtraContent.value) { + _push(ssrRenderComponent(VPFlyout, mergeProps({ + class: "VPNavBarExtra", + label: "extra navigation" + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + if (unref(localeLinks).length && unref(currentLang).label) { + _push2(`

    ${ssrInterpolate(unref(currentLang).label)}

    `); + ssrRenderList(unref(localeLinks), (locale) => { + _push2(ssrRenderComponent(VPMenuLink, { item: locale }, null, _parent2, _scopeId)); + }); + _push2(`
    `); + } else { + _push2(``); + } + if (unref(site).appearance && unref(site).appearance !== "force-dark" && unref(site).appearance !== "force-auto") { + _push2(`

    ${ssrInterpolate(unref(theme2).darkModeSwitchLabel || "Appearance")}

    `); + _push2(ssrRenderComponent(VPSwitchAppearance, null, null, _parent2, _scopeId)); + _push2(`
    `); + } else { + _push2(``); + } + if (unref(theme2).socialLinks) { + _push2(`
    `); + } else { + _push2(``); + } + } else { + return [ + unref(localeLinks).length && unref(currentLang).label ? (openBlock(), createBlock("div", { + key: 0, + class: "group translations" + }, [ + createVNode("p", { class: "trans-title" }, toDisplayString(unref(currentLang).label), 1), + (openBlock(true), createBlock(Fragment, null, renderList(unref(localeLinks), (locale) => { + return openBlock(), createBlock(VPMenuLink, { + key: locale.link, + item: locale + }, null, 8, ["item"]); + }), 128)) + ])) : createCommentVNode("", true), + unref(site).appearance && unref(site).appearance !== "force-dark" && unref(site).appearance !== "force-auto" ? (openBlock(), createBlock("div", { + key: 1, + class: "group" + }, [ + createVNode("div", { class: "item appearance" }, [ + createVNode("p", { class: "label" }, toDisplayString(unref(theme2).darkModeSwitchLabel || "Appearance"), 1), + createVNode("div", { class: "appearance-action" }, [ + createVNode(VPSwitchAppearance) + ]) + ]) + ])) : createCommentVNode("", true), + unref(theme2).socialLinks ? (openBlock(), createBlock("div", { + key: 2, + class: "group" + }, [ + createVNode("div", { class: "item social-links" }, [ + createVNode(VPSocialLinks, { + class: "social-links-list", + links: unref(theme2).socialLinks + }, null, 8, ["links"]) + ]) + ])) : createCommentVNode("", true) + ]; + } + }), + _: 1 + }, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$y = _sfc_main$y.setup; +_sfc_main$y.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarExtra.vue"); + return _sfc_setup$y ? _sfc_setup$y(props, ctx) : void 0; +}; +const VPNavBarExtra = /* @__PURE__ */ _export_sfc(_sfc_main$y, [["__scopeId", "data-v-bb2aa2f0"]]); +const _sfc_main$x = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarHamburger", + __ssrInlineRender: true, + props: { + active: { type: Boolean } + }, + emits: ["click"], + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + }; + } +}); +const _sfc_setup$x = _sfc_main$x.setup; +_sfc_main$x.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarHamburger.vue"); + return _sfc_setup$x ? _sfc_setup$x(props, ctx) : void 0; +}; +const VPNavBarHamburger = /* @__PURE__ */ _export_sfc(_sfc_main$x, [["__scopeId", "data-v-e5dd9c1c"]]); +const _sfc_main$w = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarMenuLink", + __ssrInlineRender: true, + props: { + item: {} + }, + setup(__props) { + const { page } = useData(); + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(_sfc_main$Z, mergeProps({ + class: { + VPNavBarMenuLink: true, + active: unref(isActive)( + unref(page).relativePath, + __props.item.activeMatch || __props.item.link, + !!__props.item.activeMatch + ) + }, + href: __props.item.link, + target: __props.item.target, + rel: __props.item.rel, + "no-icon": __props.item.noIcon, + tabindex: "0" + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${__props.item.text ?? ""}`); + } else { + return [ + createVNode("span", { + innerHTML: __props.item.text + }, null, 8, ["innerHTML"]) + ]; + } + }), + _: 1 + }, _parent)); + }; + } +}); +const _sfc_setup$w = _sfc_main$w.setup; +_sfc_main$w.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarMenuLink.vue"); + return _sfc_setup$w ? _sfc_setup$w(props, ctx) : void 0; +}; +const VPNavBarMenuLink = /* @__PURE__ */ _export_sfc(_sfc_main$w, [["__scopeId", "data-v-e56f3d57"]]); +const _sfc_main$v = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarMenuGroup", + __ssrInlineRender: true, + props: { + item: {} + }, + setup(__props) { + const props = __props; + const { page } = useData(); + const isChildActive = (navItem) => { + if ("component" in navItem) return false; + if ("link" in navItem) { + return isActive( + page.value.relativePath, + navItem.link, + !!props.item.activeMatch + ); + } + return navItem.items.some(isChildActive); + }; + const childrenActive = computed(() => isChildActive(props.item)); + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(VPFlyout, mergeProps({ + class: { + VPNavBarMenuGroup: true, + active: unref(isActive)(unref(page).relativePath, __props.item.activeMatch, !!__props.item.activeMatch) || childrenActive.value + }, + button: __props.item.text, + items: __props.item.items + }, _attrs), null, _parent)); + }; + } +}); +const _sfc_setup$v = _sfc_main$v.setup; +_sfc_main$v.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue"); + return _sfc_setup$v ? _sfc_setup$v(props, ctx) : void 0; +}; +const _sfc_main$u = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarMenu", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(theme2).nav) { + _push(` Main Navigation `); + ssrRenderList(unref(theme2).nav, (item) => { + _push(``); + if ("link" in item) { + _push(ssrRenderComponent(VPNavBarMenuLink, { item }, null, _parent)); + } else if ("component" in item) { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(item.component), mergeProps({ ref_for: true }, item.props), null), _parent); + } else { + _push(ssrRenderComponent(_sfc_main$v, { item }, null, _parent)); + } + _push(``); + }); + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$u = _sfc_main$u.setup; +_sfc_main$u.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarMenu.vue"); + return _sfc_setup$u ? _sfc_setup$u(props, ctx) : void 0; +}; +const VPNavBarMenu = /* @__PURE__ */ _export_sfc(_sfc_main$u, [["__scopeId", "data-v-dc692963"]]); +function createSearchTranslate(defaultTranslations) { + const { localeIndex, theme: theme2 } = useData(); + function translate(key) { + var _a, _b, _c; + const keyPath = key.split("."); + const themeObject = (_a = theme2.value.search) == null ? void 0 : _a.options; + const isObject = themeObject && typeof themeObject === "object"; + const locales = isObject && ((_c = (_b = themeObject.locales) == null ? void 0 : _b[localeIndex.value]) == null ? void 0 : _c.translations) || null; + const translations = isObject && themeObject.translations || null; + let localeResult = locales; + let translationResult = translations; + let defaultResult = defaultTranslations; + const lastKey = keyPath.pop(); + for (const k of keyPath) { + let fallbackResult = null; + const foundInFallback = defaultResult == null ? void 0 : defaultResult[k]; + if (foundInFallback) { + fallbackResult = defaultResult = foundInFallback; + } + const foundInTranslation = translationResult == null ? void 0 : translationResult[k]; + if (foundInTranslation) { + fallbackResult = translationResult = foundInTranslation; + } + const foundInLocale = localeResult == null ? void 0 : localeResult[k]; + if (foundInLocale) { + fallbackResult = localeResult = foundInLocale; + } + if (!foundInFallback) { + defaultResult = fallbackResult; + } + if (!foundInTranslation) { + translationResult = fallbackResult; + } + if (!foundInLocale) { + localeResult = fallbackResult; + } + } + return (localeResult == null ? void 0 : localeResult[lastKey]) ?? (translationResult == null ? void 0 : translationResult[lastKey]) ?? (defaultResult == null ? void 0 : defaultResult[lastKey]) ?? ""; + } + return translate; +} +const _sfc_main$t = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarSearchButton", + __ssrInlineRender: true, + setup(__props) { + const defaultTranslations = { + button: { + buttonText: "Search", + buttonAriaLabel: "Search" + } + }; + const translate = createSearchTranslate(defaultTranslations); + return (_ctx, _push, _parent, _attrs) => { + _push(`${ssrInterpolate(unref(translate)("button.buttonText"))}K`); + }; + } +}); +const _sfc_setup$t = _sfc_main$t.setup; +_sfc_main$t.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearchButton.vue"); + return _sfc_setup$t ? _sfc_setup$t(props, ctx) : void 0; +}; +const _sfc_main$s = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarSearch", + __ssrInlineRender: true, + setup(__props) { + const VPLocalSearchBox = defineAsyncComponent(() => import("./VPLocalSearchBox.BcKWVt-i.js")); + const VPAlgoliaSearchBox = () => null; + const { theme: theme2 } = useData(); + const loaded = ref(false); + const actuallyLoaded = ref(false); + onMounted(() => { + { + return; + } + }); + function load() { + if (!loaded.value) { + loaded.value = true; + setTimeout(poll, 16); + } + } + function poll() { + const e = new Event("keydown"); + e.key = "k"; + e.metaKey = true; + window.dispatchEvent(e); + setTimeout(() => { + if (!document.querySelector(".DocSearch-Modal")) { + poll(); + } + }, 16); + } + function isEditingContent(event) { + const element = event.target; + const tagName = element.tagName; + return element.isContentEditable || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA"; + } + const showSearch = ref(false); + { + onKeyStroke("k", (event) => { + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + showSearch.value = true; + } + }); + onKeyStroke("/", (event) => { + if (!isEditingContent(event)) { + event.preventDefault(); + showSearch.value = true; + } + }); + } + const provider = "local"; + return (_ctx, _push, _parent, _attrs) => { + var _a; + _push(``); + if (unref(provider) === "local") { + _push(``); + if (showSearch.value) { + _push(ssrRenderComponent(unref(VPLocalSearchBox), { + onClose: ($event) => showSearch.value = false + }, null, _parent)); + } else { + _push(``); + } + _push(``); + } else if (unref(provider) === "algolia") { + _push(``); + if (loaded.value) { + _push(ssrRenderComponent(unref(VPAlgoliaSearchBox), { + algolia: ((_a = unref(theme2).search) == null ? void 0 : _a.options) ?? unref(theme2).algolia, + onVnodeBeforeMount: ($event) => actuallyLoaded.value = true + }, null, _parent)); + } else { + _push(``); + } + if (!actuallyLoaded.value) { + _push(`
    `); + _push(ssrRenderComponent(_sfc_main$t, { onClick: load }, null, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(``); + } else { + _push(``); + } + _push(``); + }; + } +}); +const _sfc_setup$s = _sfc_main$s.setup; +_sfc_main$s.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue"); + return _sfc_setup$s ? _sfc_setup$s(props, ctx) : void 0; +}; +const _sfc_main$r = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarSocialLinks", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(theme2).socialLinks) { + _push(ssrRenderComponent(VPSocialLinks, mergeProps({ + class: "VPNavBarSocialLinks", + links: unref(theme2).socialLinks + }, _attrs), null, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$r = _sfc_main$r.setup; +_sfc_main$r.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarSocialLinks.vue"); + return _sfc_setup$r ? _sfc_setup$r(props, ctx) : void 0; +}; +const VPNavBarSocialLinks = /* @__PURE__ */ _export_sfc(_sfc_main$r, [["__scopeId", "data-v-0394ad82"]]); +const _sfc_main$q = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarTitle", + __ssrInlineRender: true, + setup(__props) { + const { site, theme: theme2 } = useData(); + const { hasSidebar } = useSidebar(); + const { currentLang } = useLangs(); + const link2 = computed( + () => { + var _a; + return typeof theme2.value.logoLink === "string" ? theme2.value.logoLink : (_a = theme2.value.logoLink) == null ? void 0 : _a.link; + } + ); + const rel = computed( + () => { + var _a; + return typeof theme2.value.logoLink === "string" ? void 0 : (_a = theme2.value.logoLink) == null ? void 0 : _a.rel; + } + ); + const target = computed( + () => { + var _a; + return typeof theme2.value.logoLink === "string" ? void 0 : (_a = theme2.value.logoLink) == null ? void 0 : _a.target; + } + ); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderSlot(_ctx.$slots, "nav-bar-title-before", {}, null, _push, _parent); + if (unref(theme2).logo) { + _push(ssrRenderComponent(VPImage, { + class: "logo", + image: unref(theme2).logo + }, null, _parent)); + } else { + _push(``); + } + if (unref(theme2).siteTitle) { + _push(`${unref(theme2).siteTitle ?? ""}`); + } else if (unref(theme2).siteTitle === void 0) { + _push(`${ssrInterpolate(unref(site).title)}`); + } else { + _push(``); + } + ssrRenderSlot(_ctx.$slots, "nav-bar-title-after", {}, null, _push, _parent); + _push(``); + }; + } +}); +const _sfc_setup$q = _sfc_main$q.setup; +_sfc_main$q.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarTitle.vue"); + return _sfc_setup$q ? _sfc_setup$q(props, ctx) : void 0; +}; +const VPNavBarTitle = /* @__PURE__ */ _export_sfc(_sfc_main$q, [["__scopeId", "data-v-1168a8e4"]]); +const _sfc_main$p = /* @__PURE__ */ defineComponent({ + __name: "VPNavBarTranslations", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + const { localeLinks, currentLang } = useLangs({ correspondingLink: true }); + return (_ctx, _push, _parent, _attrs) => { + if (unref(localeLinks).length && unref(currentLang).label) { + _push(ssrRenderComponent(VPFlyout, mergeProps({ + class: "VPNavBarTranslations", + icon: "vpi-languages", + label: unref(theme2).langMenuLabel || "Change language" + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`

    ${ssrInterpolate(unref(currentLang).label)}

    `); + ssrRenderList(unref(localeLinks), (locale) => { + _push2(ssrRenderComponent(VPMenuLink, { item: locale }, null, _parent2, _scopeId)); + }); + _push2(`
    `); + } else { + return [ + createVNode("div", { class: "items" }, [ + createVNode("p", { class: "title" }, toDisplayString(unref(currentLang).label), 1), + (openBlock(true), createBlock(Fragment, null, renderList(unref(localeLinks), (locale) => { + return openBlock(), createBlock(VPMenuLink, { + key: locale.link, + item: locale + }, null, 8, ["item"]); + }), 128)) + ]) + ]; + } + }), + _: 1 + }, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$p = _sfc_main$p.setup; +_sfc_main$p.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBarTranslations.vue"); + return _sfc_setup$p ? _sfc_setup$p(props, ctx) : void 0; +}; +const VPNavBarTranslations = /* @__PURE__ */ _export_sfc(_sfc_main$p, [["__scopeId", "data-v-88af2de4"]]); +const _sfc_main$o = /* @__PURE__ */ defineComponent({ + __name: "VPNavBar", + __ssrInlineRender: true, + props: { + isScreenOpen: { type: Boolean } + }, + emits: ["toggle-screen"], + setup(__props) { + const props = __props; + const { y } = useWindowScroll(); + const { hasSidebar } = useSidebar(); + const { frontmatter } = useData(); + const classes = ref({}); + watchPostEffect(() => { + classes.value = { + "has-sidebar": hasSidebar.value, + "home": frontmatter.value.layout === "home", + "top": y.value === 0, + "screen-open": props.isScreenOpen + }; + }); + return (_ctx, _push, _parent, _attrs) => { + _push(`
    `); + _push(ssrRenderComponent(VPNavBarTitle, null, { + "nav-bar-title-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-before", {}, void 0, true) + ]; + } + }), + "nav-bar-title-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "nav-bar-content-before", {}, null, _push, _parent); + _push(ssrRenderComponent(_sfc_main$s, { class: "search" }, null, _parent)); + _push(ssrRenderComponent(VPNavBarMenu, { class: "menu" }, null, _parent)); + _push(ssrRenderComponent(VPNavBarTranslations, { class: "translations" }, null, _parent)); + _push(ssrRenderComponent(VPNavBarAppearance, { class: "appearance" }, null, _parent)); + _push(ssrRenderComponent(VPNavBarSocialLinks, { class: "social-links" }, null, _parent)); + _push(ssrRenderComponent(VPNavBarExtra, { class: "extra" }, null, _parent)); + ssrRenderSlot(_ctx.$slots, "nav-bar-content-after", {}, null, _push, _parent); + _push(ssrRenderComponent(VPNavBarHamburger, { + class: "hamburger", + active: __props.isScreenOpen, + onClick: ($event) => _ctx.$emit("toggle-screen") + }, null, _parent)); + _push(`
    `); + }; + } +}); +const _sfc_setup$o = _sfc_main$o.setup; +_sfc_main$o.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavBar.vue"); + return _sfc_setup$o ? _sfc_setup$o(props, ctx) : void 0; +}; +const VPNavBar = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-6aa21345"]]); +const _sfc_main$n = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenAppearance", + __ssrInlineRender: true, + setup(__props) { + const { site, theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(site).appearance && unref(site).appearance !== "force-dark" && unref(site).appearance !== "force-auto") { + _push(`

    ${ssrInterpolate(unref(theme2).darkModeSwitchLabel || "Appearance")}

    `); + _push(ssrRenderComponent(VPSwitchAppearance, null, null, _parent)); + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$n = _sfc_main$n.setup; +_sfc_main$n.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenAppearance.vue"); + return _sfc_setup$n ? _sfc_setup$n(props, ctx) : void 0; +}; +const VPNavScreenAppearance = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-b44890b2"]]); +const _sfc_main$m = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenMenuLink", + __ssrInlineRender: true, + props: { + item: {} + }, + setup(__props) { + const closeScreen = inject("close-screen"); + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(_sfc_main$Z, mergeProps({ + class: "VPNavScreenMenuLink", + href: __props.item.link, + target: __props.item.target, + rel: __props.item.rel, + "no-icon": __props.item.noIcon, + onClick: unref(closeScreen) + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${__props.item.text ?? ""}`); + } else { + return [ + createVNode("span", { + innerHTML: __props.item.text + }, null, 8, ["innerHTML"]) + ]; + } + }), + _: 1 + }, _parent)); + }; + } +}); +const _sfc_setup$m = _sfc_main$m.setup; +_sfc_main$m.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenMenuLink.vue"); + return _sfc_setup$m ? _sfc_setup$m(props, ctx) : void 0; +}; +const VPNavScreenMenuLink = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-df37e6dd"]]); +const _sfc_main$l = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenMenuGroupLink", + __ssrInlineRender: true, + props: { + item: {} + }, + setup(__props) { + const closeScreen = inject("close-screen"); + return (_ctx, _push, _parent, _attrs) => { + _push(ssrRenderComponent(_sfc_main$Z, mergeProps({ + class: "VPNavScreenMenuGroupLink", + href: __props.item.link, + target: __props.item.target, + rel: __props.item.rel, + "no-icon": __props.item.noIcon, + onClick: unref(closeScreen) + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${__props.item.text ?? ""}`); + } else { + return [ + createVNode("span", { + innerHTML: __props.item.text + }, null, 8, ["innerHTML"]) + ]; + } + }), + _: 1 + }, _parent)); + }; + } +}); +const _sfc_setup$l = _sfc_main$l.setup; +_sfc_main$l.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenMenuGroupLink.vue"); + return _sfc_setup$l ? _sfc_setup$l(props, ctx) : void 0; +}; +const VPNavScreenMenuGroupLink = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-3e9c20e4"]]); +const _sfc_main$k = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenMenuGroupSection", + __ssrInlineRender: true, + props: { + text: {}, + items: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + if (__props.text) { + _push(`

    ${ssrInterpolate(__props.text)}

    `); + } else { + _push(``); + } + _push(``); + ssrRenderList(__props.items, (item) => { + _push(ssrRenderComponent(VPNavScreenMenuGroupLink, { + key: item.text, + item + }, null, _parent)); + }); + _push(``); + }; + } +}); +const _sfc_setup$k = _sfc_main$k.setup; +_sfc_main$k.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenMenuGroupSection.vue"); + return _sfc_setup$k ? _sfc_setup$k(props, ctx) : void 0; +}; +const VPNavScreenMenuGroupSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-8133b170"]]); +const _sfc_main$j = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenMenuGroup", + __ssrInlineRender: true, + props: { + text: {}, + items: {} + }, + setup(__props) { + const props = __props; + const isOpen = ref(false); + const groupId = computed( + () => `NavScreenGroup-${props.text.replace(" ", "-").toLowerCase()}` + ); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderList(__props.items, (item) => { + _push(``); + if ("link" in item) { + _push(`
    `); + _push(ssrRenderComponent(VPNavScreenMenuGroupLink, { item }, null, _parent)); + _push(`
    `); + } else if ("component" in item) { + _push(`
    `); + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(item.component), mergeProps({ ref_for: true }, item.props, { "screen-menu": "" }), null), _parent); + _push(`
    `); + } else { + _push(`
    `); + _push(ssrRenderComponent(VPNavScreenMenuGroupSection, { + text: item.text, + items: item.items + }, null, _parent)); + _push(`
    `); + } + _push(``); + }); + _push(``); + }; + } +}); +const _sfc_setup$j = _sfc_main$j.setup; +_sfc_main$j.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue"); + return _sfc_setup$j ? _sfc_setup$j(props, ctx) : void 0; +}; +const VPNavScreenMenuGroup = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-b9ab8c58"]]); +const _sfc_main$i = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenMenu", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(theme2).nav) { + _push(``); + ssrRenderList(unref(theme2).nav, (item) => { + _push(``); + if ("link" in item) { + _push(ssrRenderComponent(VPNavScreenMenuLink, { item }, null, _parent)); + } else if ("component" in item) { + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(item.component), mergeProps({ ref_for: true }, item.props, { "screen-menu": "" }), null), _parent); + } else { + _push(ssrRenderComponent(VPNavScreenMenuGroup, { + text: item.text || "", + items: item.items + }, null, _parent)); + } + _push(``); + }); + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$i = _sfc_main$i.setup; +_sfc_main$i.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenMenu.vue"); + return _sfc_setup$i ? _sfc_setup$i(props, ctx) : void 0; +}; +const _sfc_main$h = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenSocialLinks", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + return (_ctx, _push, _parent, _attrs) => { + if (unref(theme2).socialLinks) { + _push(ssrRenderComponent(VPSocialLinks, mergeProps({ + class: "VPNavScreenSocialLinks", + links: unref(theme2).socialLinks + }, _attrs), null, _parent)); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$h = _sfc_main$h.setup; +_sfc_main$h.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenSocialLinks.vue"); + return _sfc_setup$h ? _sfc_setup$h(props, ctx) : void 0; +}; +const _sfc_main$g = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreenTranslations", + __ssrInlineRender: true, + setup(__props) { + const { localeLinks, currentLang } = useLangs({ correspondingLink: true }); + const isOpen = ref(false); + return (_ctx, _push, _parent, _attrs) => { + if (unref(localeLinks).length && unref(currentLang).label) { + _push(`
      `); + ssrRenderList(unref(localeLinks), (locale) => { + _push(`
    • `); + _push(ssrRenderComponent(_sfc_main$Z, { + class: "link", + href: locale.link + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${ssrInterpolate(locale.text)}`); + } else { + return [ + createTextVNode(toDisplayString(locale.text), 1) + ]; + } + }), + _: 2 + }, _parent)); + _push(`
    • `); + }); + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$g = _sfc_main$g.setup; +_sfc_main$g.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreenTranslations.vue"); + return _sfc_setup$g ? _sfc_setup$g(props, ctx) : void 0; +}; +const VPNavScreenTranslations = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-858fe1a4"]]); +const _sfc_main$f = /* @__PURE__ */ defineComponent({ + __name: "VPNavScreen", + __ssrInlineRender: true, + props: { + open: { type: Boolean } + }, + setup(__props) { + const screen = ref(null); + useScrollLock(inBrowser ? document.body : null); + return (_ctx, _push, _parent, _attrs) => { + if (__props.open) { + _push(`
    `); + ssrRenderSlot(_ctx.$slots, "nav-screen-content-before", {}, null, _push, _parent); + _push(ssrRenderComponent(_sfc_main$i, { class: "menu" }, null, _parent)); + _push(ssrRenderComponent(VPNavScreenTranslations, { class: "translations" }, null, _parent)); + _push(ssrRenderComponent(VPNavScreenAppearance, { class: "appearance" }, null, _parent)); + _push(ssrRenderComponent(_sfc_main$h, { class: "social-links" }, null, _parent)); + ssrRenderSlot(_ctx.$slots, "nav-screen-content-after", {}, null, _push, _parent); + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$f = _sfc_main$f.setup; +_sfc_main$f.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNavScreen.vue"); + return _sfc_setup$f ? _sfc_setup$f(props, ctx) : void 0; +}; +const VPNavScreen = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["__scopeId", "data-v-f2779853"]]); +const _sfc_main$e = /* @__PURE__ */ defineComponent({ + __name: "VPNav", + __ssrInlineRender: true, + setup(__props) { + const { isScreenOpen, closeScreen, toggleScreen } = useNav(); + const { frontmatter } = useData(); + const hasNavbar = computed(() => { + return frontmatter.value.navbar !== false; + }); + provide("close-screen", closeScreen); + watchEffect(() => { + if (inBrowser) { + document.documentElement.classList.toggle("hide-nav", !hasNavbar.value); + } + }); + return (_ctx, _push, _parent, _attrs) => { + if (hasNavbar.value) { + _push(``); + _push(ssrRenderComponent(VPNavBar, { + "is-screen-open": unref(isScreenOpen), + onToggleScreen: unref(toggleScreen) + }, { + "nav-bar-title-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-before", {}, void 0, true) + ]; + } + }), + "nav-bar-title-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-after", {}, void 0, true) + ]; + } + }), + "nav-bar-content-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-content-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-content-before", {}, void 0, true) + ]; + } + }), + "nav-bar-content-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-content-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-content-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(ssrRenderComponent(VPNavScreen, { open: unref(isScreenOpen) }, { + "nav-screen-content-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-screen-content-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-screen-content-before", {}, void 0, true) + ]; + } + }), + "nav-screen-content-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-screen-content-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-screen-content-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(``); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$e = _sfc_main$e.setup; +_sfc_main$e.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPNav.vue"); + return _sfc_setup$e ? _sfc_setup$e(props, ctx) : void 0; +}; +const VPNav = /* @__PURE__ */ _export_sfc(_sfc_main$e, [["__scopeId", "data-v-ae24b3ad"]]); +const _sfc_main$d = /* @__PURE__ */ defineComponent({ + __name: "VPSidebarItem", + __ssrInlineRender: true, + props: { + item: {}, + depth: {} + }, + setup(__props) { + const props = __props; + const { + collapsed, + collapsible, + isLink, + isActiveLink, + hasActiveLink: hasActiveLink2, + hasChildren, + toggle + } = useSidebarControl(computed(() => props.item)); + const sectionTag = computed(() => hasChildren.value ? "section" : `div`); + const linkTag = computed(() => isLink.value ? "a" : "div"); + const textTag = computed(() => { + return !hasChildren.value ? "p" : props.depth + 2 === 7 ? "p" : `h${props.depth + 2}`; + }); + const itemRole = computed(() => isLink.value ? void 0 : "button"); + const classes = computed(() => [ + [`level-${props.depth}`], + { collapsible: collapsible.value }, + { collapsed: collapsed.value }, + { "is-link": isLink.value }, + { "is-active": isActiveLink.value }, + { "has-active": hasActiveLink2.value } + ]); + function onItemInteraction(e) { + if ("key" in e && e.key !== "Enter") { + return; + } + !props.item.link && toggle(); + } + function onCaretClick() { + props.item.link && toggle(); + } + return (_ctx, _push, _parent, _attrs) => { + const _component_VPSidebarItem = resolveComponent("VPSidebarItem", true); + ssrRenderVNode(_push, createVNode(resolveDynamicComponent(sectionTag.value), mergeProps({ + class: ["VPSidebarItem", classes.value] + }, _attrs), { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + if (__props.item.text) { + _push2(`
    `); + if (__props.item.link) { + _push2(ssrRenderComponent(_sfc_main$Z, { + tag: linkTag.value, + class: "link", + href: __props.item.link, + rel: __props.item.rel, + target: __props.item.target + }, { + default: withCtx((_2, _push3, _parent3, _scopeId2) => { + if (_push3) { + ssrRenderVNode(_push3, createVNode(resolveDynamicComponent(textTag.value), { class: "text" }, null), _parent3, _scopeId2); + } else { + return [ + (openBlock(), createBlock(resolveDynamicComponent(textTag.value), { + class: "text", + innerHTML: __props.item.text + }, null, 8, ["innerHTML"])) + ]; + } + }), + _: 1 + }, _parent2, _scopeId)); + } else { + ssrRenderVNode(_push2, createVNode(resolveDynamicComponent(textTag.value), { class: "text" }, null), _parent2, _scopeId); + } + if (__props.item.collapsed != null && __props.item.items && __props.item.items.length) { + _push2(`
    `); + } else { + _push2(``); + } + _push2(`
    `); + } else { + _push2(``); + } + if (__props.item.items && __props.item.items.length) { + _push2(`
    `); + if (__props.depth < 5) { + _push2(``); + ssrRenderList(__props.item.items, (i) => { + _push2(ssrRenderComponent(_component_VPSidebarItem, { + key: i.text, + item: i, + depth: __props.depth + 1 + }, null, _parent2, _scopeId)); + }); + _push2(``); + } else { + _push2(``); + } + _push2(`
    `); + } else { + _push2(``); + } + } else { + return [ + __props.item.text ? (openBlock(), createBlock("div", mergeProps({ + key: 0, + class: "item", + role: itemRole.value + }, toHandlers( + __props.item.items ? { click: onItemInteraction, keydown: onItemInteraction } : {}, + true + ), { + tabindex: __props.item.items && 0 + }), [ + createVNode("div", { class: "indicator" }), + __props.item.link ? (openBlock(), createBlock(_sfc_main$Z, { + key: 0, + tag: linkTag.value, + class: "link", + href: __props.item.link, + rel: __props.item.rel, + target: __props.item.target + }, { + default: withCtx(() => [ + (openBlock(), createBlock(resolveDynamicComponent(textTag.value), { + class: "text", + innerHTML: __props.item.text + }, null, 8, ["innerHTML"])) + ]), + _: 1 + }, 8, ["tag", "href", "rel", "target"])) : (openBlock(), createBlock(resolveDynamicComponent(textTag.value), { + key: 1, + class: "text", + innerHTML: __props.item.text + }, null, 8, ["innerHTML"])), + __props.item.collapsed != null && __props.item.items && __props.item.items.length ? (openBlock(), createBlock("div", { + key: 2, + class: "caret", + role: "button", + "aria-label": "toggle section", + onClick: onCaretClick, + onKeydown: withKeys(onCaretClick, ["enter"]), + tabindex: "0" + }, [ + createVNode("span", { class: "vpi-chevron-right caret-icon" }) + ], 32)) : createCommentVNode("", true) + ], 16, ["role", "tabindex"])) : createCommentVNode("", true), + __props.item.items && __props.item.items.length ? (openBlock(), createBlock("div", { + key: 1, + class: "items" + }, [ + __props.depth < 5 ? (openBlock(true), createBlock(Fragment, { key: 0 }, renderList(__props.item.items, (i) => { + return openBlock(), createBlock(_component_VPSidebarItem, { + key: i.text, + item: i, + depth: __props.depth + 1 + }, null, 8, ["item", "depth"]); + }), 128)) : createCommentVNode("", true) + ])) : createCommentVNode("", true) + ]; + } + }), + _: 1 + }), _parent); + }; + } +}); +const _sfc_setup$d = _sfc_main$d.setup; +_sfc_main$d.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSidebarItem.vue"); + return _sfc_setup$d ? _sfc_setup$d(props, ctx) : void 0; +}; +const VPSidebarItem = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["__scopeId", "data-v-b3fd67f8"]]); +const _sfc_main$c = /* @__PURE__ */ defineComponent({ + __name: "VPSidebarGroup", + __ssrInlineRender: true, + props: { + items: {} + }, + setup(__props) { + const disableTransition = ref(true); + let timer = null; + onMounted(() => { + timer = setTimeout(() => { + timer = null; + disableTransition.value = false; + }, 300); + }); + onBeforeUnmount(() => { + if (timer != null) { + clearTimeout(timer); + timer = null; + } + }); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderList(__props.items, (item) => { + _push(`
    `); + _push(ssrRenderComponent(VPSidebarItem, { + item, + depth: 0 + }, null, _parent)); + _push(`
    `); + }); + _push(``); + }; + } +}); +const _sfc_setup$c = _sfc_main$c.setup; +_sfc_main$c.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSidebarGroup.vue"); + return _sfc_setup$c ? _sfc_setup$c(props, ctx) : void 0; +}; +const VPSidebarGroup = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["__scopeId", "data-v-c40bc020"]]); +const _sfc_main$b = /* @__PURE__ */ defineComponent({ + __name: "VPSidebar", + __ssrInlineRender: true, + props: { + open: { type: Boolean } + }, + setup(__props) { + const { sidebarGroups, hasSidebar } = useSidebar(); + const props = __props; + const navEl = ref(null); + const isLocked = useScrollLock(inBrowser ? document.body : null); + watch( + [props, navEl], + () => { + var _a; + if (props.open) { + isLocked.value = true; + (_a = navEl.value) == null ? void 0 : _a.focus(); + } else isLocked.value = false; + }, + { immediate: true, flush: "post" } + ); + const key = ref(0); + watch( + sidebarGroups, + () => { + key.value += 1; + }, + { deep: true } + ); + return (_ctx, _push, _parent, _attrs) => { + if (unref(hasSidebar)) { + _push(`
    `); + } else { + _push(``); + } + }; + } +}); +const _sfc_setup$b = _sfc_main$b.setup; +_sfc_main$b.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSidebar.vue"); + return _sfc_setup$b ? _sfc_setup$b(props, ctx) : void 0; +}; +const VPSidebar = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "data-v-319d5ca6"]]); +const _sfc_main$a = /* @__PURE__ */ defineComponent({ + __name: "VPSkipLink", + __ssrInlineRender: true, + setup(__props) { + const { theme: theme2 } = useData(); + const route = useRoute(); + const backToTop = ref(); + watch(() => route.path, () => backToTop.value.focus()); + return (_ctx, _push, _parent, _attrs) => { + _push(`${ssrInterpolate(unref(theme2).skipToContentLabel || "Skip to content")}`); + }; + } +}); +const _sfc_setup$a = _sfc_main$a.setup; +_sfc_main$a.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSkipLink.vue"); + return _sfc_setup$a ? _sfc_setup$a(props, ctx) : void 0; +}; +const VPSkipLink = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-0b0ada53"]]); +const _sfc_main$9 = /* @__PURE__ */ defineComponent({ + __name: "Layout", + __ssrInlineRender: true, + setup(__props) { + const { + isOpen: isSidebarOpen, + open: openSidebar, + close: closeSidebar + } = useSidebar(); + const route = useRoute(); + watch(() => route.path, closeSidebar); + useCloseSidebarOnEscape(isSidebarOpen, closeSidebar); + const { frontmatter } = useData(); + const slots = useSlots(); + const heroImageSlotExists = computed(() => !!slots["home-hero-image"]); + provide("hero-image-slot-exists", heroImageSlotExists); + return (_ctx, _push, _parent, _attrs) => { + const _component_Content = resolveComponent("Content"); + if (unref(frontmatter).layout !== false) { + _push(``); + ssrRenderSlot(_ctx.$slots, "layout-top", {}, null, _push, _parent); + _push(ssrRenderComponent(VPSkipLink, null, null, _parent)); + _push(ssrRenderComponent(VPBackdrop, { + class: "backdrop", + show: unref(isSidebarOpen), + onClick: unref(closeSidebar) + }, null, _parent)); + _push(ssrRenderComponent(VPNav, null, { + "nav-bar-title-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-before", {}, void 0, true) + ]; + } + }), + "nav-bar-title-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-title-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-title-after", {}, void 0, true) + ]; + } + }), + "nav-bar-content-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-content-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-content-before", {}, void 0, true) + ]; + } + }), + "nav-bar-content-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-bar-content-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-bar-content-after", {}, void 0, true) + ]; + } + }), + "nav-screen-content-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-screen-content-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-screen-content-before", {}, void 0, true) + ]; + } + }), + "nav-screen-content-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "nav-screen-content-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "nav-screen-content-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(ssrRenderComponent(VPLocalNav, { + open: unref(isSidebarOpen), + onOpenMenu: unref(openSidebar) + }, null, _parent)); + _push(ssrRenderComponent(VPSidebar, { open: unref(isSidebarOpen) }, { + "sidebar-nav-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "sidebar-nav-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "sidebar-nav-before", {}, void 0, true) + ]; + } + }), + "sidebar-nav-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "sidebar-nav-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "sidebar-nav-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(ssrRenderComponent(VPContent, null, { + "page-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "page-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "page-top", {}, void 0, true) + ]; + } + }), + "page-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "page-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "page-bottom", {}, void 0, true) + ]; + } + }), + "not-found": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "not-found", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "not-found", {}, void 0, true) + ]; + } + }), + "home-hero-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-before", {}, void 0, true) + ]; + } + }), + "home-hero-info-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-before", {}, void 0, true) + ]; + } + }), + "home-hero-info": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info", {}, void 0, true) + ]; + } + }), + "home-hero-info-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-info-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-info-after", {}, void 0, true) + ]; + } + }), + "home-hero-actions-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-actions-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-actions-after", {}, void 0, true) + ]; + } + }), + "home-hero-image": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-image", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-image", {}, void 0, true) + ]; + } + }), + "home-hero-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-hero-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-hero-after", {}, void 0, true) + ]; + } + }), + "home-features-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-features-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-features-before", {}, void 0, true) + ]; + } + }), + "home-features-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "home-features-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "home-features-after", {}, void 0, true) + ]; + } + }), + "doc-footer-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-footer-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-footer-before", {}, void 0, true) + ]; + } + }), + "doc-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-before", {}, void 0, true) + ]; + } + }), + "doc-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-after", {}, void 0, true) + ]; + } + }), + "doc-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-top", {}, void 0, true) + ]; + } + }), + "doc-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "doc-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "doc-bottom", {}, void 0, true) + ]; + } + }), + "aside-top": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-top", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-top", {}, void 0, true) + ]; + } + }), + "aside-bottom": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-bottom", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-bottom", {}, void 0, true) + ]; + } + }), + "aside-outline-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-before", {}, void 0, true) + ]; + } + }), + "aside-outline-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-outline-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-outline-after", {}, void 0, true) + ]; + } + }), + "aside-ads-before": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-before", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-before", {}, void 0, true) + ]; + } + }), + "aside-ads-after": withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + ssrRenderSlot(_ctx.$slots, "aside-ads-after", {}, null, _push2, _parent2, _scopeId); + } else { + return [ + renderSlot(_ctx.$slots, "aside-ads-after", {}, void 0, true) + ]; + } + }), + _: 3 + }, _parent)); + _push(ssrRenderComponent(VPFooter, null, null, _parent)); + ssrRenderSlot(_ctx.$slots, "layout-bottom", {}, null, _push, _parent); + _push(``); + } else { + _push(ssrRenderComponent(_component_Content, _attrs, null, _parent)); + } + }; + } +}); +const _sfc_setup$9 = _sfc_main$9.setup; +_sfc_main$9.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/Layout.vue"); + return _sfc_setup$9 ? _sfc_setup$9(props, ctx) : void 0; +}; +const Layout = /* @__PURE__ */ _export_sfc(_sfc_main$9, [["__scopeId", "data-v-5d98c3a5"]]); +const GridSettings = { + xmini: [[0, 2]], + mini: [], + small: [ + [920, 6], + [768, 5], + [640, 4], + [480, 3], + [0, 2] + ], + medium: [ + [960, 5], + [832, 4], + [640, 3], + [480, 2] + ], + big: [ + [832, 3], + [640, 2] + ] +}; +function useSponsorsGrid({ el, size = "medium" }) { + const onResize = throttleAndDebounce(manage, 100); + onMounted(() => { + manage(); + window.addEventListener("resize", onResize); + }); + onUnmounted(() => { + window.removeEventListener("resize", onResize); + }); + function manage() { + adjustSlots(el.value, size); + } +} +function adjustSlots(el, size) { + const tsize = el.children.length; + const asize = el.querySelectorAll(".vp-sponsor-grid-item:not(.empty)").length; + const grid = setGrid(el, size, asize); + manageSlots(el, grid, tsize, asize); +} +function setGrid(el, size, items) { + const settings = GridSettings[size]; + const screen = window.innerWidth; + let grid = 1; + settings.some(([breakpoint, value]) => { + if (screen >= breakpoint) { + grid = items < value ? items : value; + return true; + } + }); + setGridData(el, grid); + return grid; +} +function setGridData(el, value) { + el.dataset.vpGrid = String(value); +} +function manageSlots(el, grid, tsize, asize) { + const diff = tsize - asize; + const rem = asize % grid; + const drem = rem === 0 ? rem : grid - rem; + neutralizeSlots(el, drem - diff); +} +function neutralizeSlots(el, count) { + if (count === 0) { + return; + } + count > 0 ? addSlots(el, count) : removeSlots(el, count * -1); +} +function addSlots(el, count) { + for (let i = 0; i < count; i++) { + const slot = document.createElement("div"); + slot.classList.add("vp-sponsor-grid-item", "empty"); + el.append(slot); + } +} +function removeSlots(el, count) { + for (let i = 0; i < count; i++) { + el.removeChild(el.lastElementChild); + } +} +const _sfc_main$8 = /* @__PURE__ */ defineComponent({ + __name: "VPSponsorsGrid", + __ssrInlineRender: true, + props: { + size: { default: "medium" }, + data: {} + }, + setup(__props) { + const props = __props; + const el = ref(null); + useSponsorsGrid({ el, size: props.size }); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderList(__props.data, (sponsor) => { + _push(``); + }); + _push(``); + }; + } +}); +const _sfc_setup$8 = _sfc_main$8.setup; +_sfc_main$8.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSponsorsGrid.vue"); + return _sfc_setup$8 ? _sfc_setup$8(props, ctx) : void 0; +}; +const _sfc_main$7 = /* @__PURE__ */ defineComponent({ + __name: "VPSponsors", + __ssrInlineRender: true, + props: { + mode: { default: "normal" }, + tier: {}, + size: {}, + data: {} + }, + setup(__props) { + const props = __props; + const sponsors = computed(() => { + const isSponsors = props.data.some((s) => { + return "items" in s; + }); + if (isSponsors) { + return props.data; + } + return [ + { tier: props.tier, size: props.size, items: props.data } + ]; + }); + return (_ctx, _push, _parent, _attrs) => { + _push(``); + ssrRenderList(sponsors.value, (sponsor, index) => { + _push(`
    `); + if (sponsor.tier) { + _push(`

    ${ssrInterpolate(sponsor.tier)}

    `); + } else { + _push(``); + } + _push(ssrRenderComponent(_sfc_main$8, { + size: sponsor.size, + data: sponsor.items + }, null, _parent)); + _push(`
    `); + }); + _push(``); + }; + } +}); +const _sfc_setup$7 = _sfc_main$7.setup; +_sfc_main$7.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue"); + return _sfc_setup$7 ? _sfc_setup$7(props, ctx) : void 0; +}; +const _sfc_main$6 = /* @__PURE__ */ defineComponent({ + __name: "VPDocAsideSponsors", + __ssrInlineRender: true, + props: { + tier: {}, + size: {}, + data: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(``); + _push(ssrRenderComponent(_sfc_main$7, { + mode: "aside", + tier: __props.tier, + size: __props.size, + data: __props.data + }, null, _parent)); + _push(``); + }; + } +}); +const _sfc_setup$6 = _sfc_main$6.setup; +_sfc_main$6.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue"); + return _sfc_setup$6 ? _sfc_setup$6(props, ctx) : void 0; +}; +const _sfc_main$5 = /* @__PURE__ */ defineComponent({ + __name: "VPHomeSponsors", + __ssrInlineRender: true, + props: { + message: {}, + actionText: { default: "Become a sponsor" }, + actionLink: {}, + data: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(`
    `); + if (__props.message) { + _push(`

    ${ssrInterpolate(__props.message)}

    `); + } else { + _push(``); + } + _push(`
    `); + _push(ssrRenderComponent(_sfc_main$7, { data: __props.data }, null, _parent)); + _push(`
    `); + if (__props.actionLink) { + _push(`
    `); + _push(ssrRenderComponent(VPButton, { + theme: "sponsor", + text: __props.actionText, + href: __props.actionLink + }, null, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(`
    `); + }; + } +}); +const _sfc_setup$5 = _sfc_main$5.setup; +_sfc_main$5.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue"); + return _sfc_setup$5 ? _sfc_setup$5(props, ctx) : void 0; +}; +const _sfc_main$4 = /* @__PURE__ */ defineComponent({ + __name: "VPTeamMembersItem", + __ssrInlineRender: true, + props: { + size: { default: "medium" }, + member: {} + }, + setup(__props) { + return (_ctx, _push, _parent, _attrs) => { + _push(`

    ${ssrInterpolate(__props.member.name)}

    `); + if (__props.member.title || __props.member.org) { + _push(`

    `); + if (__props.member.title) { + _push(`${ssrInterpolate(__props.member.title)}`); + } else { + _push(``); + } + if (__props.member.title && __props.member.org) { + _push(` @ `); + } else { + _push(``); + } + if (__props.member.org) { + _push(ssrRenderComponent(_sfc_main$Z, { + class: ["org", { link: __props.member.orgLink }], + href: __props.member.orgLink, + "no-icon": "" + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(`${ssrInterpolate(__props.member.org)}`); + } else { + return [ + createTextVNode(toDisplayString(__props.member.org), 1) + ]; + } + }), + _: 1 + }, _parent)); + } else { + _push(``); + } + _push(`

    `); + } else { + _push(``); + } + if (__props.member.desc) { + _push(`

    ${__props.member.desc ?? ""}

    `); + } else { + _push(``); + } + if (__props.member.links) { + _push(``); + } else { + _push(``); + } + _push(`
    `); + if (__props.member.sponsor) { + _push(`
    `); + _push(ssrRenderComponent(_sfc_main$Z, { + class: "sp-link", + href: __props.member.sponsor, + "no-icon": "" + }, { + default: withCtx((_, _push2, _parent2, _scopeId) => { + if (_push2) { + _push2(` ${ssrInterpolate(__props.member.actionText || "Sponsor")}`); + } else { + return [ + createVNode("span", { class: "vpi-heart sp-icon" }), + createTextVNode(" " + toDisplayString(__props.member.actionText || "Sponsor"), 1) + ]; + } + }), + _: 1 + }, _parent)); + _push(`
    `); + } else { + _push(``); + } + _push(``); + }; + } +}); +const _sfc_setup$4 = _sfc_main$4.setup; +_sfc_main$4.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPTeamMembersItem.vue"); + return _sfc_setup$4 ? _sfc_setup$4(props, ctx) : void 0; +}; +const VPTeamMembersItem = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["__scopeId", "data-v-f3fa364a"]]); +const _sfc_main$3 = /* @__PURE__ */ defineComponent({ + __name: "VPTeamMembers", + __ssrInlineRender: true, + props: { + size: { default: "medium" }, + members: {} + }, + setup(__props) { + const props = __props; + const classes = computed(() => [props.size, `count-${props.members.length}`]); + return (_ctx, _push, _parent, _attrs) => { + _push(`
    `); + ssrRenderList(__props.members, (member) => { + _push(`
    `); + _push(ssrRenderComponent(VPTeamMembersItem, { + size: __props.size, + member + }, null, _parent)); + _push(`
    `); + }); + _push(`
    `); + }; + } +}); +const _sfc_setup$3 = _sfc_main$3.setup; +_sfc_main$3.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue"); + return _sfc_setup$3 ? _sfc_setup$3(props, ctx) : void 0; +}; +const _sfc_main$2 = {}; +const _sfc_setup$2 = _sfc_main$2.setup; +_sfc_main$2.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue"); + return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0; +}; +const _sfc_main$1 = {}; +const _sfc_setup$1 = _sfc_main$1.setup; +_sfc_main$1.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue"); + return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0; +}; +const _sfc_main = {}; +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("../node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const theme = { + Layout, + enhanceApp: ({ app }) => { + app.component("Badge", _sfc_main$14); + } +}; +const ClientOnly = defineComponent({ + setup(_, { slots }) { + const show = ref(false); + onMounted(() => { + show.value = true; + }); + return () => show.value && slots.default ? slots.default() : null; + } +}); +function useCodeGroups() { + if (inBrowser) { + window.addEventListener("click", (e) => { + var _a; + const el = e.target; + if (el.matches(".vp-code-group input")) { + const group = (_a = el.parentElement) == null ? void 0 : _a.parentElement; + if (!group) + return; + const i = Array.from(group.querySelectorAll("input")).indexOf(el); + if (i < 0) + return; + const blocks = group.querySelector(".blocks"); + if (!blocks) + return; + const current = Array.from(blocks.children).find((child) => child.classList.contains("active")); + if (!current) + return; + const next = blocks.children[i]; + if (!next || current === next) + return; + current.classList.remove("active"); + next.classList.add("active"); + const label = group == null ? void 0 : group.querySelector(`label[for="${el.id}"]`); + label == null ? void 0 : label.scrollIntoView({ block: "nearest" }); + } + }); + } +} +function useCopyCode() { + if (inBrowser) { + const timeoutIdMap = /* @__PURE__ */ new WeakMap(); + window.addEventListener("click", (e) => { + var _a; + const el = e.target; + if (el.matches('div[class*="language-"] > button.copy')) { + const parent = el.parentElement; + const sibling = (_a = el.nextElementSibling) == null ? void 0 : _a.nextElementSibling; + if (!parent || !sibling) { + return; + } + const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test(parent.className); + const ignoredNodes = [".vp-copy-ignore", ".diff.remove"]; + const clone = sibling.cloneNode(true); + clone.querySelectorAll(ignoredNodes.join(",")).forEach((node) => node.remove()); + let text = clone.textContent || ""; + if (isShell) { + text = text.replace(/^ *(\$|>) /gm, "").trim(); + } + copyToClipboard(text).then(() => { + el.classList.add("copied"); + clearTimeout(timeoutIdMap.get(el)); + const timeoutId = setTimeout(() => { + el.classList.remove("copied"); + el.blur(); + timeoutIdMap.delete(el); + }, 2e3); + timeoutIdMap.set(el, timeoutId); + }); + } + }); + } +} +async function copyToClipboard(text) { + try { + return navigator.clipboard.writeText(text); + } catch { + const element = document.createElement("textarea"); + const previouslyFocusedElement = document.activeElement; + element.value = text; + element.setAttribute("readonly", ""); + element.style.contain = "strict"; + element.style.position = "absolute"; + element.style.left = "-9999px"; + element.style.fontSize = "12pt"; + const selection = document.getSelection(); + const originalRange = selection ? selection.rangeCount > 0 && selection.getRangeAt(0) : null; + document.body.appendChild(element); + element.select(); + element.selectionStart = 0; + element.selectionEnd = text.length; + document.execCommand("copy"); + document.body.removeChild(element); + if (originalRange) { + selection.removeAllRanges(); + selection.addRange(originalRange); + } + if (previouslyFocusedElement) { + previouslyFocusedElement.focus(); + } + } +} +function useUpdateHead(route, siteDataByRouteRef) { + let isFirstUpdate = true; + let managedHeadElements = []; + const updateHeadTags = (newTags) => { + if (isFirstUpdate) { + isFirstUpdate = false; + newTags.forEach((tag) => { + const headEl = createHeadElement(tag); + for (const el of document.head.children) { + if (el.isEqualNode(headEl)) { + managedHeadElements.push(el); + return; + } + } + }); + return; + } + const newElements = newTags.map(createHeadElement); + managedHeadElements.forEach((oldEl, oldIndex) => { + const matchedIndex = newElements.findIndex((newEl) => newEl == null ? void 0 : newEl.isEqualNode(oldEl ?? null)); + if (matchedIndex !== -1) { + delete newElements[matchedIndex]; + } else { + oldEl == null ? void 0 : oldEl.remove(); + delete managedHeadElements[oldIndex]; + } + }); + newElements.forEach((el) => el && document.head.appendChild(el)); + managedHeadElements = [...managedHeadElements, ...newElements].filter(Boolean); + }; + watchEffect(() => { + const pageData = route.data; + const siteData2 = siteDataByRouteRef.value; + const pageDescription = pageData && pageData.description; + const frontmatterHead = pageData && pageData.frontmatter.head || []; + const title = createTitle(siteData2, pageData); + if (title !== document.title) { + document.title = title; + } + const description = pageDescription || siteData2.description; + let metaDescriptionElement = document.querySelector(`meta[name=description]`); + if (metaDescriptionElement) { + if (metaDescriptionElement.getAttribute("content") !== description) { + metaDescriptionElement.setAttribute("content", description); + } + } else { + createHeadElement(["meta", { name: "description", content: description }]); + } + updateHeadTags(mergeHead(siteData2.head, filterOutHeadDescription(frontmatterHead))); + }); +} +function createHeadElement([tag, attrs, innerHTML]) { + const el = document.createElement(tag); + for (const key in attrs) { + el.setAttribute(key, attrs[key]); + } + if (innerHTML) { + el.innerHTML = innerHTML; + } + if (tag === "script" && attrs.async == null) { + el.async = false; + } + return el; +} +function isMetaDescription(headConfig) { + return headConfig[0] === "meta" && headConfig[1] && headConfig[1].name === "description"; +} +function filterOutHeadDescription(head) { + return head.filter((h2) => !isMetaDescription(h2)); +} +const hasFetched = /* @__PURE__ */ new Set(); +const createLink = () => document.createElement("link"); +const viaDOM = (url) => { + const link2 = createLink(); + link2.rel = `prefetch`; + link2.href = url; + document.head.appendChild(link2); +}; +const viaXHR = (url) => { + const req = new XMLHttpRequest(); + req.open("GET", url, req.withCredentials = true); + req.send(); +}; +let link; +const doFetch = inBrowser && (link = createLink()) && link.relList && link.relList.supports && link.relList.supports("prefetch") ? viaDOM : viaXHR; +function usePrefetch() { + if (!inBrowser) { + return; + } + if (!window.IntersectionObserver) { + return; + } + let conn; + if ((conn = navigator.connection) && (conn.saveData || /2g/.test(conn.effectiveType))) { + return; + } + const rIC = window.requestIdleCallback || setTimeout; + let observer = null; + const observeLinks = () => { + if (observer) { + observer.disconnect(); + } + observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const link2 = entry.target; + observer.unobserve(link2); + const { pathname } = link2; + if (!hasFetched.has(pathname)) { + hasFetched.add(pathname); + const pageChunkPath = pathToFile(pathname); + if (pageChunkPath) + doFetch(pageChunkPath); + } + } + }); + }); + rIC(() => { + document.querySelectorAll("#app a").forEach((link2) => { + const { hostname, pathname } = new URL(link2.href instanceof SVGAnimatedString ? link2.href.animVal : link2.href, link2.baseURI); + const extMatch = pathname.match(/\.\w+$/); + if (extMatch && extMatch[0] !== ".html") { + return; + } + if ( + // only prefetch same tab navigation, since a new tab will load + // the lean js chunk instead. + link2.target !== "_blank" && // only prefetch inbound links + hostname === location.hostname + ) { + if (pathname !== location.pathname) { + observer.observe(link2); + } else { + hasFetched.add(pathname); + } + } + }); + }); + }; + onMounted(observeLinks); + const route = useRoute(); + watch(() => route.path, observeLinks); + onUnmounted(() => { + observer && observer.disconnect(); + }); +} +function resolveThemeExtends(theme2) { + if (theme2.extends) { + const base = resolveThemeExtends(theme2.extends); + return { + ...base, + ...theme2, + async enhanceApp(ctx) { + if (base.enhanceApp) + await base.enhanceApp(ctx); + if (theme2.enhanceApp) + await theme2.enhanceApp(ctx); + } + }; + } + return theme2; +} +const Theme = resolveThemeExtends(theme); +const VitePressApp = defineComponent({ + name: "VitePressApp", + setup() { + const { site, lang, dir } = useData$1(); + onMounted(() => { + watchEffect(() => { + document.documentElement.lang = lang.value; + document.documentElement.dir = dir.value; + }); + }); + if (site.value.router.prefetchLinks) { + usePrefetch(); + } + useCopyCode(); + useCodeGroups(); + if (Theme.setup) + Theme.setup(); + return () => h(Theme.Layout); + } +}); +async function createApp() { + globalThis.__VITEPRESS__ = true; + const router = newRouter(); + const app = newApp(); + app.provide(RouterSymbol, router); + const data = initData(router.route); + app.provide(dataSymbol, data); + app.component("Content", Content); + app.component("ClientOnly", ClientOnly); + Object.defineProperties(app.config.globalProperties, { + $frontmatter: { + get() { + return data.frontmatter.value; + } + }, + $params: { + get() { + return data.page.value.params; + } + } + }); + if (Theme.enhanceApp) { + await Theme.enhanceApp({ + app, + router, + siteData: siteDataRef + }); + } + return { app, router, data }; +} +function newApp() { + return createSSRApp(VitePressApp); +} +function newRouter() { + let isInitialPageLoad = inBrowser; + return createRouter((path) => { + let pageFilePath = pathToFile(path); + let pageModule = null; + if (pageFilePath) { + if (isInitialPageLoad) { + pageFilePath = pageFilePath.replace(/\.js$/, ".lean.js"); + } + if (false) ; + else { + pageModule = import( + /*@vite-ignore*/ + pageFilePath + ); + } + } + if (inBrowser) { + isInitialPageLoad = false; + } + return pageModule; + }, Theme.NotFound); +} +if (inBrowser) { + createApp().then(({ app, router, data }) => { + router.go().then(() => { + useUpdateHead(router.route, data.site); + app.mount("#app"); + }); + }); +} +async function render(path) { + const { app, router } = await createApp(); + await router.go(path); + const ctx = { content: "", vpSocialIcons: /* @__PURE__ */ new Set() }; + ctx.content = await renderToString(app, ctx); + return ctx; +} +export { + useRouter as a, + createSearchTranslate as c, + dataSymbol as d, + escapeRegExp as e, + inBrowser as i, + pathToFile as p, + render, + useData as u +}; diff --git a/docs/.vitepress/.temp/assets/style.ClG4-ikt.css b/docs/.vitepress/.temp/assets/style.ClG4-ikt.css new file mode 100644 index 0000000..477c658 --- /dev/null +++ b/docs/.vitepress/.temp/assets/style.ClG4-ikt.css @@ -0,0 +1,5095 @@ + + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-cyrillic.C5lxZ8CY.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-greek-ext.CqjqNYQ-.woff2') format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-greek.BBVDIX6e.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, + U+03A3-03FF; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-vietnamese.BjW4sHH5.woff2') format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, + U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, + U+0329, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-latin-ext.4ZJIpNVo.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: Inter; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-roman-latin.Di8DUHzh.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-cyrillic-ext.r48I6akx.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-cyrillic.By2_1cv3.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-greek-ext.1u6EdAuj.woff2') format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-greek.DJ8dCoTZ.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, + U+03A3-03FF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-vietnamese.BSbpV94h.woff2') format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, + U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, + U+0329, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-latin-ext.CN1xVJS-.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, + U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: Inter; + font-style: italic; + font-weight: 100 900; + font-display: swap; + src: url('/ai-coding-kit/assets/inter-italic-latin.C2AdPX0b.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, + U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Punctuation SC'; + font-weight: 400; + src: local('PingFang SC Regular'), local('Noto Sans CJK SC'), + local('Microsoft YaHei'); + unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026, + U+00B7, U+007E, U+002F; +} + +@font-face { + font-family: 'Punctuation SC'; + font-weight: 500; + src: local('PingFang SC Medium'), local('Noto Sans CJK SC'), + local('Microsoft YaHei'); + unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026, + U+00B7, U+007E, U+002F; +} + +@font-face { + font-family: 'Punctuation SC'; + font-weight: 600; + src: local('PingFang SC Semibold'), local('Noto Sans CJK SC Bold'), + local('Microsoft YaHei Bold'); + unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026, + U+00B7, U+007E, U+002F; +} + +@font-face { + font-family: 'Punctuation SC'; + font-weight: 700; + src: local('PingFang SC Semibold'), local('Noto Sans CJK SC Bold'), + local('Microsoft YaHei Bold'); + unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026, + U+00B7, U+007E, U+002F; +} + +/* Generate the subsetted fonts using: `pyftsubset .woff2 --unicodes="" --output-file="inter-\3c style>-.woff2" --flavor=woff2` */ +/** + * Colors: Solid + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-white: #ffffff; + --vp-c-black: #000000; + + --vp-c-neutral: var(--vp-c-black); + --vp-c-neutral-inverse: var(--vp-c-white); +} + +.dark { + --vp-c-neutral: var(--vp-c-white); + --vp-c-neutral-inverse: var(--vp-c-black); +} + +/** + * Colors: Palette + * + * The primitive colors used for accent colors. These colors are referenced + * by functional colors such as "Text", "Background", or "Brand". + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create a accent, such as when having inline code block inside + * custom containers. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-gray-1: #dddde3; + --vp-c-gray-2: #e4e4e9; + --vp-c-gray-3: #ebebef; + --vp-c-gray-soft: rgba(142, 150, 170, 0.14); + + --vp-c-indigo-1: #3451b2; + --vp-c-indigo-2: #3a5ccc; + --vp-c-indigo-3: #5672cd; + --vp-c-indigo-soft: rgba(100, 108, 255, 0.14); + + --vp-c-purple-1: #6f42c1; + --vp-c-purple-2: #7e4cc9; + --vp-c-purple-3: #8e5cd9; + --vp-c-purple-soft: rgba(159, 122, 234, 0.14); + + --vp-c-green-1: #18794e; + --vp-c-green-2: #299764; + --vp-c-green-3: #30a46c; + --vp-c-green-soft: rgba(16, 185, 129, 0.14); + + --vp-c-yellow-1: #915930; + --vp-c-yellow-2: #946300; + --vp-c-yellow-3: #9f6a00; + --vp-c-yellow-soft: rgba(234, 179, 8, 0.14); + + --vp-c-red-1: #b8272c; + --vp-c-red-2: #d5393e; + --vp-c-red-3: #e0575b; + --vp-c-red-soft: rgba(244, 63, 94, 0.14); + + --vp-c-sponsor: #db2777; +} + +.dark { + --vp-c-gray-1: #515c67; + --vp-c-gray-2: #414853; + --vp-c-gray-3: #32363f; + --vp-c-gray-soft: rgba(101, 117, 133, 0.16); + + --vp-c-indigo-1: #a8b1ff; + --vp-c-indigo-2: #5c73e7; + --vp-c-indigo-3: #3e63dd; + --vp-c-indigo-soft: rgba(100, 108, 255, 0.16); + + --vp-c-purple-1: #c8abfa; + --vp-c-purple-2: #a879e6; + --vp-c-purple-3: #8e5cd9; + --vp-c-purple-soft: rgba(159, 122, 234, 0.16); + + --vp-c-green-1: #3dd68c; + --vp-c-green-2: #30a46c; + --vp-c-green-3: #298459; + --vp-c-green-soft: rgba(16, 185, 129, 0.16); + + --vp-c-yellow-1: #f9b44e; + --vp-c-yellow-2: #da8b17; + --vp-c-yellow-3: #a46a0a; + --vp-c-yellow-soft: rgba(234, 179, 8, 0.16); + + --vp-c-red-1: #f66f81; + --vp-c-red-2: #f14158; + --vp-c-red-3: #b62a3c; + --vp-c-red-soft: rgba(244, 63, 94, 0.16); +} + +/** + * Colors: Background + * + * - `bg`: The bg color used for main screen. + * + * - `bg-alt`: The alternative bg color used in places such as "sidebar", + * or "code block". + * + * - `bg-elv`: The elevated bg color. This is used at parts where it "floats", + * such as "dialog". + * + * - `bg-soft`: The bg color to slightly distinguish some components from + * the page. Used for things like "carbon ads" or "table". + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-bg: #ffffff; + --vp-c-bg-alt: #f6f6f7; + --vp-c-bg-elv: #ffffff; + --vp-c-bg-soft: #f6f6f7; +} + +.dark { + --vp-c-bg: #1b1b1f; + --vp-c-bg-alt: #161618; + --vp-c-bg-elv: #202127; + --vp-c-bg-soft: #202127; +} + +/** + * Colors: Borders + * + * - `divider`: This is used for separators. This is used to divide sections + * within the same components, such as having separator on "h2" heading. + * + * - `border`: This is designed for borders on interactive components. + * For example this should be used for a button outline. + * + * - `gutter`: This is used to divide components in the page. For example + * the header and the lest of the page. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-border: #c2c2c4; + --vp-c-divider: #e2e2e3; + --vp-c-gutter: #e2e2e3; +} + +.dark { + --vp-c-border: #3c3f44; + --vp-c-divider: #2e2e32; + --vp-c-gutter: #000000; +} + +/** + * Colors: Text + * + * - `text-1`: Used for primary text. + * + * - `text-2`: Used for muted texts, such as "inactive menu" or "info texts". + * + * - `text-3`: Used for subtle texts, such as "placeholders" or "caret icon". + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-text-1: #3c3c43; + --vp-c-text-2: #67676c; + --vp-c-text-3: #929295; +} + +.dark { + --vp-c-text-1: #dfdfd6; + --vp-c-text-2: #98989f; + --vp-c-text-3: #6a6a71; +} + +/** + * Colors: Function + * + * - `default`: The color used purely for subtle indication without any + * special meanings attached to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * + * To understand the scaling system, refer to "Colors: Palette" section. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: var(--vp-c-indigo-1); + --vp-c-brand-2: var(--vp-c-indigo-2); + --vp-c-brand-3: var(--vp-c-indigo-3); + --vp-c-brand-soft: var(--vp-c-indigo-soft); + + /* DEPRECATED: Use `--vp-c-brand-1` instead. */ + --vp-c-brand: var(--vp-c-brand-1); + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-note-1: var(--vp-c-brand-1); + --vp-c-note-2: var(--vp-c-brand-2); + --vp-c-note-3: var(--vp-c-brand-3); + --vp-c-note-soft: var(--vp-c-brand-soft); + + --vp-c-success-1: var(--vp-c-green-1); + --vp-c-success-2: var(--vp-c-green-2); + --vp-c-success-3: var(--vp-c-green-3); + --vp-c-success-soft: var(--vp-c-green-soft); + + --vp-c-important-1: var(--vp-c-purple-1); + --vp-c-important-2: var(--vp-c-purple-2); + --vp-c-important-3: var(--vp-c-purple-3); + --vp-c-important-soft: var(--vp-c-purple-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); + + --vp-c-caution-1: var(--vp-c-red-1); + --vp-c-caution-2: var(--vp-c-red-2); + --vp-c-caution-3: var(--vp-c-red-3); + --vp-c-caution-soft: var(--vp-c-red-soft); +} + +/** + * Typography + * -------------------------------------------------------------------------- */ + +:root { + --vp-font-family-base: 'Inter', ui-sans-serif, system-ui, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --vp-font-family-mono: ui-monospace, 'Menlo', 'Monaco', 'Consolas', + 'Liberation Mono', 'Courier New', monospace; + font-optical-sizing: auto; +} + +:root:where(:lang(zh)) { + --vp-font-family-base: 'Punctuation SC', 'Inter', ui-sans-serif, system-ui, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; +} + +/** + * Shadows + * -------------------------------------------------------------------------- */ + +:root { + --vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06); + --vp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.07), 0 1px 4px rgba(0, 0, 0, 0.07); + --vp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08); + --vp-shadow-4: 0 14px 44px rgba(0, 0, 0, 0.12), 0 3px 9px rgba(0, 0, 0, 0.12); + --vp-shadow-5: 0 18px 56px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.16); +} + +/** + * Z-indexes + * -------------------------------------------------------------------------- */ + +:root { + --vp-z-index-footer: 10; + --vp-z-index-local-nav: 20; + --vp-z-index-nav: 30; + --vp-z-index-layout-top: 40; + --vp-z-index-backdrop: 50; + --vp-z-index-sidebar: 60; +} + +@media (min-width: 960px) { + :root { + --vp-z-index-sidebar: 25; + } +} + +/** + * Layouts + * -------------------------------------------------------------------------- */ + +:root { + --vp-layout-max-width: 1440px; +} + +/** + * Component: Header Anchor + * -------------------------------------------------------------------------- */ + +:root { + --vp-header-anchor-symbol: '#'; +} + +/** + * Component: Code + * -------------------------------------------------------------------------- */ + +:root { + --vp-code-line-height: 1.7; + --vp-code-font-size: 0.875em; + --vp-code-color: var(--vp-c-brand-1); + --vp-code-link-color: var(--vp-c-brand-1); + --vp-code-link-hover-color: var(--vp-c-brand-2); + --vp-code-bg: var(--vp-c-default-soft); + + --vp-code-block-color: var(--vp-c-text-2); + --vp-code-block-bg: var(--vp-c-bg-alt); + --vp-code-block-divider-color: var(--vp-c-gutter); + + --vp-code-lang-color: var(--vp-c-text-3); + + --vp-code-line-highlight-color: var(--vp-c-default-soft); + --vp-code-line-number-color: var(--vp-c-text-3); + + --vp-code-line-diff-add-color: var(--vp-c-success-soft); + --vp-code-line-diff-add-symbol-color: var(--vp-c-success-1); + + --vp-code-line-diff-remove-color: var(--vp-c-danger-soft); + --vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1); + + --vp-code-line-warning-color: var(--vp-c-warning-soft); + --vp-code-line-error-color: var(--vp-c-danger-soft); + + --vp-code-copy-code-border-color: var(--vp-c-divider); + --vp-code-copy-code-bg: var(--vp-c-bg-soft); + --vp-code-copy-code-hover-border-color: var(--vp-c-divider); + --vp-code-copy-code-hover-bg: var(--vp-c-bg); + --vp-code-copy-code-active-text: var(--vp-c-text-2); + --vp-code-copy-copied-text-content: 'Copied'; + + --vp-code-tab-divider: var(--vp-code-block-divider-color); + --vp-code-tab-text-color: var(--vp-c-text-2); + --vp-code-tab-bg: var(--vp-code-block-bg); + --vp-code-tab-hover-text-color: var(--vp-c-text-1); + --vp-code-tab-active-text-color: var(--vp-c-text-1); + --vp-code-tab-active-bar-color: var(--vp-c-brand-1); +} + +:lang(es), +:lang(pt) { + --vp-code-copy-copied-text-content: 'Copiado'; +} +:lang(fa) { + --vp-code-copy-copied-text-content: 'کپی شد'; +} +:lang(ko) { + --vp-code-copy-copied-text-content: '복사됨'; +} +:lang(ru) { + --vp-code-copy-copied-text-content: 'Скопировано'; +} +:lang(zh) { + --vp-code-copy-copied-text-content: '已复制'; +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); + + --vp-button-alt-border: transparent; + --vp-button-alt-text: var(--vp-c-text-1); + --vp-button-alt-bg: var(--vp-c-default-3); + --vp-button-alt-hover-border: transparent; + --vp-button-alt-hover-text: var(--vp-c-text-1); + --vp-button-alt-hover-bg: var(--vp-c-default-2); + --vp-button-alt-active-border: transparent; + --vp-button-alt-active-text: var(--vp-c-text-1); + --vp-button-alt-active-bg: var(--vp-c-default-1); + + --vp-button-sponsor-border: var(--vp-c-text-2); + --vp-button-sponsor-text: var(--vp-c-text-2); + --vp-button-sponsor-bg: transparent; + --vp-button-sponsor-hover-border: var(--vp-c-sponsor); + --vp-button-sponsor-hover-text: var(--vp-c-sponsor); + --vp-button-sponsor-hover-bg: transparent; + --vp-button-sponsor-active-border: var(--vp-c-sponsor); + --vp-button-sponsor-active-text: var(--vp-c-sponsor); + --vp-button-sponsor-active-bg: transparent; +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-font-size: 14px; + --vp-custom-block-code-font-size: 13px; + + --vp-custom-block-info-border: transparent; + --vp-custom-block-info-text: var(--vp-c-text-1); + --vp-custom-block-info-bg: var(--vp-c-default-soft); + --vp-custom-block-info-code-bg: var(--vp-c-default-soft); + + --vp-custom-block-note-border: transparent; + --vp-custom-block-note-text: var(--vp-c-text-1); + --vp-custom-block-note-bg: var(--vp-c-default-soft); + --vp-custom-block-note-code-bg: var(--vp-c-default-soft); + + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-tip-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-tip-soft); + + --vp-custom-block-important-border: transparent; + --vp-custom-block-important-text: var(--vp-c-text-1); + --vp-custom-block-important-bg: var(--vp-c-important-soft); + --vp-custom-block-important-code-bg: var(--vp-c-important-soft); + + --vp-custom-block-warning-border: transparent; + --vp-custom-block-warning-text: var(--vp-c-text-1); + --vp-custom-block-warning-bg: var(--vp-c-warning-soft); + --vp-custom-block-warning-code-bg: var(--vp-c-warning-soft); + + --vp-custom-block-danger-border: transparent; + --vp-custom-block-danger-text: var(--vp-c-text-1); + --vp-custom-block-danger-bg: var(--vp-c-danger-soft); + --vp-custom-block-danger-code-bg: var(--vp-c-danger-soft); + + --vp-custom-block-caution-border: transparent; + --vp-custom-block-caution-text: var(--vp-c-text-1); + --vp-custom-block-caution-bg: var(--vp-c-caution-soft); + --vp-custom-block-caution-code-bg: var(--vp-c-caution-soft); + + --vp-custom-block-details-border: var(--vp-custom-block-info-border); + --vp-custom-block-details-text: var(--vp-custom-block-info-text); + --vp-custom-block-details-bg: var(--vp-custom-block-info-bg); + --vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg); +} + +/** + * Component: Input + * -------------------------------------------------------------------------- */ + +:root { + --vp-input-border-color: var(--vp-c-border); + --vp-input-bg-color: var(--vp-c-bg-alt); + + --vp-input-switch-bg-color: var(--vp-c-default-soft); +} + +/** + * Component: Nav + * -------------------------------------------------------------------------- */ + +:root { + --vp-nav-height: 64px; + --vp-nav-bg-color: var(--vp-c-bg); + --vp-nav-screen-bg-color: var(--vp-c-bg); + --vp-nav-logo-height: 24px; +} + +.hide-nav { + --vp-nav-height: 0px; +} + +.hide-nav .VPSidebar { + --vp-nav-height: 22px; +} + +/** + * Component: Local Nav + * -------------------------------------------------------------------------- */ + +:root { + --vp-local-nav-bg-color: var(--vp-c-bg); +} + +/** + * Component: Sidebar + * -------------------------------------------------------------------------- */ + +:root { + --vp-sidebar-width: 272px; + --vp-sidebar-bg-color: var(--vp-c-bg-alt); +} + +/** + * Colors Backdrop + * -------------------------------------------------------------------------- */ + +:root { + --vp-backdrop-bg-color: rgba(0, 0, 0, 0.6); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: var(--vp-c-brand-1); + --vp-home-hero-name-background: transparent; + + --vp-home-hero-image-background-image: none; + --vp-home-hero-image-filter: none; +} + +/** + * Component: Badge + * -------------------------------------------------------------------------- */ + +:root { + --vp-badge-info-border: transparent; + --vp-badge-info-text: var(--vp-c-text-2); + --vp-badge-info-bg: var(--vp-c-default-soft); + + --vp-badge-tip-border: transparent; + --vp-badge-tip-text: var(--vp-c-tip-1); + --vp-badge-tip-bg: var(--vp-c-tip-soft); + + --vp-badge-warning-border: transparent; + --vp-badge-warning-text: var(--vp-c-warning-1); + --vp-badge-warning-bg: var(--vp-c-warning-soft); + + --vp-badge-danger-border: transparent; + --vp-badge-danger-text: var(--vp-c-danger-1); + --vp-badge-danger-bg: var(--vp-c-danger-soft); +} + +/** + * Component: Carbon Ads + * -------------------------------------------------------------------------- */ + +:root { + --vp-carbon-ads-text-color: var(--vp-c-text-1); + --vp-carbon-ads-poweredby-color: var(--vp-c-text-2); + --vp-carbon-ads-bg-color: var(--vp-c-bg-soft); + --vp-carbon-ads-hover-text-color: var(--vp-c-brand-1); + --vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1); +} + +/** + * Component: Local Search + * -------------------------------------------------------------------------- */ + +:root { + --vp-local-search-bg: var(--vp-c-bg); + --vp-local-search-result-bg: var(--vp-c-bg); + --vp-local-search-result-border: var(--vp-c-divider); + --vp-local-search-result-selected-bg: var(--vp-c-bg); + --vp-local-search-result-selected-border: var(--vp-c-brand-1); + --vp-local-search-highlight-bg: var(--vp-c-brand-1); + --vp-local-search-highlight-text: var(--vp-c-neutral-inverse); +} +@media (prefers-reduced-motion: reduce) { + *, + ::before, + ::after { + animation-delay: -1ms !important; + animation-duration: 1ms !important; + animation-iteration-count: 1 !important; + background-attachment: initial !important; + scroll-behavior: auto !important; + transition-duration: 0s !important; + transition-delay: 0s !important; + } +} + +*, +::before, +::after { + box-sizing: border-box; +} + +html { + line-height: 1.4; + font-size: 16px; + -webkit-text-size-adjust: 100%; +} + +html.dark { + color-scheme: dark; +} + +body { + margin: 0; + width: 100%; + min-width: 320px; + min-height: 100vh; + line-height: 24px; + font-family: var(--vp-font-family-base); + font-size: 16px; + font-weight: 400; + color: var(--vp-c-text-1); + background-color: var(--vp-c-bg); + font-synthesis: style; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +main { + display: block; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + line-height: 24px; + font-size: 16px; + font-weight: 400; +} + +p { + margin: 0; +} + +strong, +b { + font-weight: 600; +} + +/** + * Avoid 300ms click delay on touch devices that support the `touch-action` + * CSS property. + * + * In particular, unlike most other browsers, IE11+Edge on Windows 10 on + * touch devices and IE Mobile 10-11 DON'T remove the click delay when + * `` is present. + * However, they DO support removing the click delay via + * `touch-action: manipulation`. + * + * See: + * - http://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch + * - http://caniuse.com/#feat=css-touch-action + * - http://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay + */ +a, +area, +button, +[role='button'], +input, +label, +select, +summary, +textarea { + touch-action: manipulation; +} + +a { + color: inherit; + text-decoration: inherit; +} + +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} + +blockquote { + margin: 0; +} + +pre, +code, +kbd, +samp { + font-family: var(--vp-font-family-mono); +} + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; +} + +figure { + margin: 0; +} + +img, +video { + max-width: 100%; + height: auto; +} + +button, +input, +optgroup, +select, +textarea { + border: 0; + padding: 0; + line-height: inherit; + color: inherit; +} + +button { + padding: 0; + font-family: inherit; + background-color: transparent; + background-image: none; +} + +button:enabled, +[role='button']:enabled { + cursor: pointer; +} + +button:focus, +button:focus-visible { + outline: 1px dotted; + outline: 4px auto -webkit-focus-ring-color; +} + +button:focus:not(:focus-visible) { + outline: none !important; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; +} + +table { + border-collapse: collapse; +} + +input { + background-color: transparent; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: var(--vp-c-text-3); +} + +input::-ms-input-placeholder, +textarea::-ms-input-placeholder { + color: var(--vp-c-text-3); +} + +input::placeholder, +textarea::placeholder { + color: var(--vp-c-text-3); +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; +} + +textarea { + resize: vertical; +} + +select { + -webkit-appearance: none; +} + +fieldset { + margin: 0; + padding: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6, +li, +p { + overflow-wrap: break-word; +} + +vite-error-overlay { + z-index: 9999; +} + +mjx-container { + overflow-x: auto; +} + +mjx-container > svg { + display: inline-block; + margin: auto; +} +[class^='vpi-'], +[class*=' vpi-'], +.vp-icon { + width: 1em; + height: 1em; +} +[class^='vpi-'].bg, +[class*=' vpi-'].bg, +.vp-icon.bg { + background-size: 100% 100%; + background-color: transparent; +} +[class^='vpi-']:not(.bg), +[class*=' vpi-']:not(.bg), +.vp-icon:not(.bg) { + -webkit-mask: var(--icon) no-repeat; + mask: var(--icon) no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + background-color: currentColor; + color: inherit; +} + +/* internal icons - used under ISC from https://lucide.dev/ */ +.vpi-align-left { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E"); +} +.vpi-arrow-right, +.vpi-arrow-down, +.vpi-arrow-left, +.vpi-arrow-up { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E"); +} +.vpi-chevron-right, +.vpi-chevron-down, +.vpi-chevron-left, +.vpi-chevron-up { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E"); +} +.vpi-chevron-down, +.vpi-arrow-down { + /*rtl:ignore*/ + transform: rotate(90deg); +} +.vpi-chevron-left, +.vpi-arrow-left { + /*rtl:ignore*/ + transform: rotate(180deg); +} +.vpi-chevron-up, +.vpi-arrow-up { + /*rtl:ignore*/ + transform: rotate(-90deg); +} +.vpi-square-pen { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E"); +} +.vpi-plus { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E"); +} +.vpi-sun { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E"); +} +.vpi-moon { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E"); +} +.vpi-more-horizontal { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E"); +} +.vpi-languages { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E"); +} +.vpi-heart { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E"); +} +.vpi-search { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E"); +} +.vpi-layout-list { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E"); +} +.vpi-delete { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E"); +} +.vpi-corner-down-left { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E"); +} +:root { + /* clipboard */ + --vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E"); + /* clipboard-copy */ + --vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E"); +} +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + white-space: nowrap; + clip: rect(0 0 0 0); + clip-path: inset(50%); + overflow: hidden; +} +.custom-block { + border: 1px solid transparent; + border-radius: 8px; + padding: 16px 16px 8px; + line-height: 24px; + font-size: var(--vp-custom-block-font-size); + color: var(--vp-c-text-2); +} + +.custom-block.info { + border-color: var(--vp-custom-block-info-border); + color: var(--vp-custom-block-info-text); + background-color: var(--vp-custom-block-info-bg); +} + +.custom-block.info a, +.custom-block.info code { + color: var(--vp-c-brand-1); +} + +.custom-block.info a:hover, +.custom-block.info a:hover > code { + color: var(--vp-c-brand-2); +} + +.custom-block.info code { + background-color: var(--vp-custom-block-info-code-bg); +} + +.custom-block.note { + border-color: var(--vp-custom-block-note-border); + color: var(--vp-custom-block-note-text); + background-color: var(--vp-custom-block-note-bg); +} + +.custom-block.note a, +.custom-block.note code { + color: var(--vp-c-brand-1); +} + +.custom-block.note a:hover, +.custom-block.note a:hover > code { + color: var(--vp-c-brand-2); +} + +.custom-block.note code { + background-color: var(--vp-custom-block-note-code-bg); +} + +.custom-block.tip { + border-color: var(--vp-custom-block-tip-border); + color: var(--vp-custom-block-tip-text); + background-color: var(--vp-custom-block-tip-bg); +} + +.custom-block.tip a, +.custom-block.tip code { + color: var(--vp-c-tip-1); +} + +.custom-block.tip a:hover, +.custom-block.tip a:hover > code { + color: var(--vp-c-tip-2); +} + +.custom-block.tip code { + background-color: var(--vp-custom-block-tip-code-bg); +} + +.custom-block.important { + border-color: var(--vp-custom-block-important-border); + color: var(--vp-custom-block-important-text); + background-color: var(--vp-custom-block-important-bg); +} + +.custom-block.important a, +.custom-block.important code { + color: var(--vp-c-important-1); +} + +.custom-block.important a:hover, +.custom-block.important a:hover > code { + color: var(--vp-c-important-2); +} + +.custom-block.important code { + background-color: var(--vp-custom-block-important-code-bg); +} + +.custom-block.warning { + border-color: var(--vp-custom-block-warning-border); + color: var(--vp-custom-block-warning-text); + background-color: var(--vp-custom-block-warning-bg); +} + +.custom-block.warning a, +.custom-block.warning code { + color: var(--vp-c-warning-1); +} + +.custom-block.warning a:hover, +.custom-block.warning a:hover > code { + color: var(--vp-c-warning-2); +} + +.custom-block.warning code { + background-color: var(--vp-custom-block-warning-code-bg); +} + +.custom-block.danger { + border-color: var(--vp-custom-block-danger-border); + color: var(--vp-custom-block-danger-text); + background-color: var(--vp-custom-block-danger-bg); +} + +.custom-block.danger a, +.custom-block.danger code { + color: var(--vp-c-danger-1); +} + +.custom-block.danger a:hover, +.custom-block.danger a:hover > code { + color: var(--vp-c-danger-2); +} + +.custom-block.danger code { + background-color: var(--vp-custom-block-danger-code-bg); +} + +.custom-block.caution { + border-color: var(--vp-custom-block-caution-border); + color: var(--vp-custom-block-caution-text); + background-color: var(--vp-custom-block-caution-bg); +} + +.custom-block.caution a, +.custom-block.caution code { + color: var(--vp-c-caution-1); +} + +.custom-block.caution a:hover, +.custom-block.caution a:hover > code { + color: var(--vp-c-caution-2); +} + +.custom-block.caution code { + background-color: var(--vp-custom-block-caution-code-bg); +} + +.custom-block.details { + border-color: var(--vp-custom-block-details-border); + color: var(--vp-custom-block-details-text); + background-color: var(--vp-custom-block-details-bg); +} + +.custom-block.details a { + color: var(--vp-c-brand-1); +} + +.custom-block.details a:hover, +.custom-block.details a:hover > code { + color: var(--vp-c-brand-2); +} + +.custom-block.details code { + background-color: var(--vp-custom-block-details-code-bg); +} + +.custom-block-title { + font-weight: 600; +} + +.custom-block p + p { + margin: 8px 0; +} + +.custom-block.details summary { + margin: 0 0 8px; + font-weight: 700; + cursor: pointer; + user-select: none; +} + +.custom-block.details summary + p { + margin: 8px 0; +} + +.custom-block a { + color: inherit; + font-weight: 600; + text-decoration: underline; + text-underline-offset: 2px; + transition: opacity 0.25s; +} + +.custom-block a:hover { + opacity: 0.75; +} + +.custom-block code { + font-size: var(--vp-custom-block-code-font-size); +} + +.custom-block.custom-block th, +.custom-block.custom-block blockquote > p { + font-size: var(--vp-custom-block-font-size); + color: inherit; +} +.dark .vp-code span { + color: var(--shiki-dark, inherit); +} + +html:not(.dark) .vp-code span { + color: var(--shiki-light, inherit); +} +.vp-code-group { + margin-top: 16px; +} + +.vp-code-group .tabs { + position: relative; + display: flex; + margin-right: -24px; + margin-left: -24px; + padding: 0 12px; + background-color: var(--vp-code-tab-bg); + overflow-x: auto; + overflow-y: hidden; + box-shadow: inset 0 -1px var(--vp-code-tab-divider); +} + +@media (min-width: 640px) { + .vp-code-group .tabs { + margin-right: 0; + margin-left: 0; + border-radius: 8px 8px 0 0; + } +} + +.vp-code-group .tabs input { + position: fixed; + opacity: 0; + pointer-events: none; +} + +.vp-code-group .tabs label { + position: relative; + display: inline-block; + border-bottom: 1px solid transparent; + padding: 0 12px; + line-height: 48px; + font-size: 14px; + font-weight: 500; + color: var(--vp-code-tab-text-color); + white-space: nowrap; + cursor: pointer; + transition: color 0.25s; +} + +.vp-code-group .tabs label::after { + position: absolute; + right: 8px; + bottom: -1px; + left: 8px; + z-index: 1; + height: 2px; + border-radius: 2px; + content: ''; + background-color: transparent; + transition: background-color 0.25s; +} + +.vp-code-group label:hover { + color: var(--vp-code-tab-hover-text-color); +} + +.vp-code-group input:checked + label { + color: var(--vp-code-tab-active-text-color); +} + +.vp-code-group input:checked + label::after { + background-color: var(--vp-code-tab-active-bar-color); +} + +.vp-code-group div[class*='language-'], +.vp-block { + display: none; + margin-top: 0 !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.vp-code-group div[class*='language-'].active, +.vp-block.active { + display: block; +} + +.vp-block { + padding: 20px 24px; +} +/** + * Headings + * -------------------------------------------------------------------------- */ + +.vp-doc h1, +.vp-doc h2, +.vp-doc h3, +.vp-doc h4, +.vp-doc h5, +.vp-doc h6 { + position: relative; + font-weight: 600; + outline: none; +} + +.vp-doc h1 { + letter-spacing: -0.02em; + line-height: 40px; + font-size: 28px; +} + +.vp-doc h2 { + margin: 48px 0 16px; + border-top: 1px solid var(--vp-c-divider); + padding-top: 24px; + letter-spacing: -0.02em; + line-height: 32px; + font-size: 24px; +} + +.vp-doc h3 { + margin: 32px 0 0; + letter-spacing: -0.01em; + line-height: 28px; + font-size: 20px; +} + +.vp-doc h4 { + margin: 24px 0 0; + letter-spacing: -0.01em; + line-height: 24px; + font-size: 18px; +} + +.vp-doc .header-anchor { + position: absolute; + top: 0; + left: 0; + margin-left: -0.87em; + font-weight: 500; + user-select: none; + opacity: 0; + text-decoration: none; + transition: + color 0.25s, + opacity 0.25s; +} + +.vp-doc .header-anchor:before { + content: var(--vp-header-anchor-symbol); +} + +.vp-doc h1:hover .header-anchor, +.vp-doc h1 .header-anchor:focus, +.vp-doc h2:hover .header-anchor, +.vp-doc h2 .header-anchor:focus, +.vp-doc h3:hover .header-anchor, +.vp-doc h3 .header-anchor:focus, +.vp-doc h4:hover .header-anchor, +.vp-doc h4 .header-anchor:focus, +.vp-doc h5:hover .header-anchor, +.vp-doc h5 .header-anchor:focus, +.vp-doc h6:hover .header-anchor, +.vp-doc h6 .header-anchor:focus { + opacity: 1; +} + +@media (min-width: 768px) { + .vp-doc h1 { + letter-spacing: -0.02em; + line-height: 40px; + font-size: 32px; + } +} + +.vp-doc h2 .header-anchor { + top: 24px; +} + +/** + * Paragraph and inline elements + * -------------------------------------------------------------------------- */ + +.vp-doc p, +.vp-doc summary { + margin: 16px 0; +} + +.vp-doc p { + line-height: 28px; +} + +.vp-doc blockquote { + margin: 16px 0; + border-left: 2px solid var(--vp-c-divider); + padding-left: 16px; + transition: border-color 0.5s; + color: var(--vp-c-text-2); +} + +.vp-doc blockquote > p { + margin: 0; + font-size: 16px; + transition: color 0.5s; +} + +.vp-doc a { + font-weight: 500; + color: var(--vp-c-brand-1); + text-decoration: underline; + text-underline-offset: 2px; + transition: + color 0.25s, + opacity 0.25s; +} + +.vp-doc a:hover { + color: var(--vp-c-brand-2); +} + +.vp-doc strong { + font-weight: 600; +} + +/** + * Lists + * -------------------------------------------------------------------------- */ + +.vp-doc ul, +.vp-doc ol { + padding-left: 1.25rem; + margin: 16px 0; +} + +.vp-doc ul { + list-style: disc; +} + +.vp-doc ol { + list-style: decimal; +} + +.vp-doc li + li { + margin-top: 8px; +} + +.vp-doc li > ol, +.vp-doc li > ul { + margin: 8px 0 0; +} + +/** + * Table + * -------------------------------------------------------------------------- */ + +.vp-doc table { + display: block; + border-collapse: collapse; + margin: 20px 0; + overflow-x: auto; +} + +.vp-doc tr { + background-color: var(--vp-c-bg); + border-top: 1px solid var(--vp-c-divider); + transition: background-color 0.5s; +} + +.vp-doc tr:nth-child(2n) { + background-color: var(--vp-c-bg-soft); +} + +.vp-doc th, +.vp-doc td { + border: 1px solid var(--vp-c-divider); + padding: 8px 16px; +} + +.vp-doc th { + text-align: left; + font-size: 14px; + font-weight: 600; + color: var(--vp-c-text-2); + background-color: var(--vp-c-bg-soft); +} + +.vp-doc td { + font-size: 14px; +} + +/** + * Decorational elements + * -------------------------------------------------------------------------- */ + +.vp-doc hr { + margin: 16px 0; + border: none; + border-top: 1px solid var(--vp-c-divider); +} + +/** + * Custom Block + * -------------------------------------------------------------------------- */ + +.vp-doc .custom-block { + margin: 16px 0; +} + +.vp-doc .custom-block p { + margin: 8px 0; + line-height: 24px; +} + +.vp-doc .custom-block p:first-child { + margin: 0; +} + +.vp-doc .custom-block div[class*='language-'] { + margin: 8px 0; + border-radius: 8px; +} + +.vp-doc .custom-block div[class*='language-'] code { + font-weight: 400; + background-color: transparent; +} + +.vp-doc .custom-block .vp-code-group .tabs { + margin: 0; + border-radius: 8px 8px 0 0; +} + +/** + * Code + * -------------------------------------------------------------------------- */ + +/* inline code */ +.vp-doc :not(pre, h1, h2, h3, h4, h5, h6) > code { + font-size: var(--vp-code-font-size); + color: var(--vp-code-color); +} + +.vp-doc :not(pre) > code { + border-radius: 4px; + padding: 3px 6px; + background-color: var(--vp-code-bg); + transition: + color 0.25s, + background-color 0.5s; +} + +.vp-doc a > code { + color: var(--vp-code-link-color); +} + +.vp-doc a:hover > code { + color: var(--vp-code-link-hover-color); +} + +.vp-doc h1 > code, +.vp-doc h2 > code, +.vp-doc h3 > code, +.vp-doc h4 > code { + font-size: 0.9em; +} + +.vp-doc div[class*='language-'], +.vp-block { + position: relative; + margin: 16px -24px; + background-color: var(--vp-code-block-bg); + overflow-x: auto; + transition: background-color 0.5s; +} + +@media (min-width: 640px) { + .vp-doc div[class*='language-'], + .vp-block { + border-radius: 8px; + margin: 16px 0; + } +} + +@media (max-width: 639px) { + .vp-doc li div[class*='language-'] { + border-radius: 8px 0 0 8px; + } +} + +.vp-doc div[class*='language-'] + div[class*='language-'], +.vp-doc div[class$='-api'] + div[class*='language-'], +.vp-doc div[class*='language-'] + div[class$='-api'] > div[class*='language-'] { + margin-top: -8px; +} + +.vp-doc [class*='language-'] pre, +.vp-doc [class*='language-'] code { + /*rtl:ignore*/ + direction: ltr; + /*rtl:ignore*/ + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +.vp-doc [class*='language-'] pre { + position: relative; + z-index: 1; + margin: 0; + padding: 20px 0; + background: transparent; + overflow-x: auto; +} + +.vp-doc [class*='language-'] code { + display: block; + padding: 0 24px; + width: fit-content; + min-width: 100%; + line-height: var(--vp-code-line-height); + font-size: var(--vp-code-font-size); + color: var(--vp-code-block-color); + transition: color 0.5s; +} + +.vp-doc [class*='language-'] code .highlighted { + background-color: var(--vp-code-line-highlight-color); + transition: background-color 0.5s; + margin: 0 -24px; + padding: 0 24px; + width: calc(100% + 2 * 24px); + display: inline-block; +} + +.vp-doc [class*='language-'] code .highlighted.error { + background-color: var(--vp-code-line-error-color); +} + +.vp-doc [class*='language-'] code .highlighted.warning { + background-color: var(--vp-code-line-warning-color); +} + +.vp-doc [class*='language-'] code .diff { + transition: background-color 0.5s; + margin: 0 -24px; + padding: 0 24px; + width: calc(100% + 2 * 24px); + display: inline-block; +} + +.vp-doc [class*='language-'] code .diff::before { + position: absolute; + left: 10px; +} + +.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { + filter: blur(0.095rem); + opacity: 0.4; + transition: + filter 0.35s, + opacity 0.35s; +} + +.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) { + opacity: 0.7; + transition: + filter 0.35s, + opacity 0.35s; +} + +.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) { + filter: blur(0); + opacity: 1; +} + +.vp-doc [class*='language-'] code .diff.remove { + background-color: var(--vp-code-line-diff-remove-color); + opacity: 0.7; +} + +.vp-doc [class*='language-'] code .diff.remove::before { + content: '-'; + color: var(--vp-code-line-diff-remove-symbol-color); +} + +.vp-doc [class*='language-'] code .diff.add { + background-color: var(--vp-code-line-diff-add-color); +} + +.vp-doc [class*='language-'] code .diff.add::before { + content: '+'; + color: var(--vp-code-line-diff-add-symbol-color); +} + +.vp-doc div[class*='language-'].line-numbers-mode { + /*rtl:ignore*/ + padding-left: 32px; +} + +.vp-doc .line-numbers-wrapper { + position: absolute; + top: 0; + bottom: 0; + /*rtl:ignore*/ + left: 0; + z-index: 3; + /*rtl:ignore*/ + border-right: 1px solid var(--vp-code-block-divider-color); + padding-top: 20px; + width: 32px; + text-align: center; + font-family: var(--vp-font-family-mono); + line-height: var(--vp-code-line-height); + font-size: var(--vp-code-font-size); + color: var(--vp-code-line-number-color); + transition: + border-color 0.5s, + color 0.5s; +} + +.vp-doc [class*='language-'] > button.copy { + /*rtl:ignore*/ + direction: ltr; + position: absolute; + top: 12px; + /*rtl:ignore*/ + right: 12px; + z-index: 3; + border: 1px solid var(--vp-code-copy-code-border-color); + border-radius: 4px; + width: 40px; + height: 40px; + background-color: var(--vp-code-copy-code-bg); + opacity: 0; + cursor: pointer; + background-image: var(--vp-icon-copy); + background-position: 50%; + background-size: 20px; + background-repeat: no-repeat; + transition: + border-color 0.25s, + background-color 0.25s, + opacity 0.25s; +} + +.vp-doc [class*='language-']:hover > button.copy, +.vp-doc [class*='language-'] > button.copy:focus { + opacity: 1; +} + +.vp-doc [class*='language-'] > button.copy:hover, +.vp-doc [class*='language-'] > button.copy.copied { + border-color: var(--vp-code-copy-code-hover-border-color); + background-color: var(--vp-code-copy-code-hover-bg); +} + +.vp-doc [class*='language-'] > button.copy.copied, +.vp-doc [class*='language-'] > button.copy:hover.copied { + /*rtl:ignore*/ + border-radius: 0 4px 4px 0; + background-color: var(--vp-code-copy-code-hover-bg); + background-image: var(--vp-icon-copied); +} + +.vp-doc [class*='language-'] > button.copy.copied::before, +.vp-doc [class*='language-'] > button.copy:hover.copied::before { + position: relative; + top: -1px; + /*rtl:ignore*/ + transform: translateX(calc(-100% - 1px)); + display: flex; + justify-content: center; + align-items: center; + border: 1px solid var(--vp-code-copy-code-hover-border-color); + /*rtl:ignore*/ + border-right: 0; + /*rtl:ignore*/ + border-radius: 4px 0 0 4px; + padding: 0 10px; + width: fit-content; + height: 40px; + text-align: center; + font-size: 12px; + font-weight: 500; + color: var(--vp-code-copy-code-active-text); + background-color: var(--vp-code-copy-code-hover-bg); + white-space: nowrap; + content: var(--vp-code-copy-copied-text-content); +} + +.vp-doc [class*='language-'] > span.lang { + position: absolute; + top: 2px; + /*rtl:ignore*/ + right: 8px; + z-index: 2; + font-size: 12px; + font-weight: 500; + user-select: none; + color: var(--vp-code-lang-color); + transition: + color 0.4s, + opacity 0.4s; +} + +.vp-doc [class*='language-']:hover > button.copy + span.lang, +.vp-doc [class*='language-'] > button.copy:focus + span.lang { + opacity: 0; +} + +/** + * Component: Team + * -------------------------------------------------------------------------- */ + +.vp-doc .VPTeamMembers { + margin-top: 24px; +} + +.vp-doc .VPTeamMembers.small.count-1 .container { + margin: 0 !important; + max-width: calc((100% - 24px) / 2) !important; +} + +.vp-doc .VPTeamMembers.small.count-2 .container, +.vp-doc .VPTeamMembers.small.count-3 .container { + max-width: 100% !important; +} + +.vp-doc .VPTeamMembers.medium.count-1 .container { + margin: 0 !important; + max-width: calc((100% - 24px) / 2) !important; +} + +/** + * External links + * -------------------------------------------------------------------------- */ + +/* prettier-ignore */ +:is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after { + display: inline-block; + margin-top: -1px; + margin-left: 4px; + width: 11px; + height: 11px; + background: currentColor; + color: var(--vp-c-text-3); + flex-shrink: 0; + --icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E"); + -webkit-mask-image: var(--icon); + mask-image: var(--icon); + /*rtl:raw:transform: scaleX(-1);*/ +} + +.vp-external-link-icon::after { + content: ''; +} + +/* prettier-ignore */ +.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(:is(.no-icon, svg a, :has(img, svg)))::after { + content: ''; + color: currentColor; +} +/** + * VPSponsors styles are defined as global because a new class gets + * allied in onMounted` hook and we can't use scoped style. + */ +.vp-sponsor { + border-radius: 16px; + overflow: hidden; +} + +.vp-sponsor.aside { + border-radius: 12px; +} + +.vp-sponsor-section + .vp-sponsor-section { + margin-top: 4px; +} + +.vp-sponsor-tier { + margin: 0 0 4px !important; + text-align: center; + letter-spacing: 1px !important; + line-height: 24px; + width: 100%; + font-weight: 600; + color: var(--vp-c-text-2); + background-color: var(--vp-c-bg-soft); +} + +.vp-sponsor.normal .vp-sponsor-tier { + padding: 13px 0 11px; + font-size: 14px; +} + +.vp-sponsor.aside .vp-sponsor-tier { + padding: 9px 0 7px; + font-size: 12px; +} + +.vp-sponsor-grid + .vp-sponsor-tier { + margin-top: 4px; +} + +.vp-sponsor-grid { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.vp-sponsor-grid.xmini .vp-sponsor-grid-link { + height: 64px; +} +.vp-sponsor-grid.xmini .vp-sponsor-grid-image { + max-width: 64px; + max-height: 22px; +} + +.vp-sponsor-grid.mini .vp-sponsor-grid-link { + height: 72px; +} +.vp-sponsor-grid.mini .vp-sponsor-grid-image { + max-width: 96px; + max-height: 24px; +} + +.vp-sponsor-grid.small .vp-sponsor-grid-link { + height: 96px; +} +.vp-sponsor-grid.small .vp-sponsor-grid-image { + max-width: 96px; + max-height: 24px; +} + +.vp-sponsor-grid.medium .vp-sponsor-grid-link { + height: 112px; +} +.vp-sponsor-grid.medium .vp-sponsor-grid-image { + max-width: 120px; + max-height: 36px; +} + +.vp-sponsor-grid.big .vp-sponsor-grid-link { + height: 184px; +} +.vp-sponsor-grid.big .vp-sponsor-grid-image { + max-width: 192px; + max-height: 56px; +} + +.vp-sponsor-grid[data-vp-grid='2'] .vp-sponsor-grid-item { + width: calc((100% - 4px) / 2); +} + +.vp-sponsor-grid[data-vp-grid='3'] .vp-sponsor-grid-item { + width: calc((100% - 4px * 2) / 3); +} + +.vp-sponsor-grid[data-vp-grid='4'] .vp-sponsor-grid-item { + width: calc((100% - 4px * 3) / 4); +} + +.vp-sponsor-grid[data-vp-grid='5'] .vp-sponsor-grid-item { + width: calc((100% - 4px * 4) / 5); +} + +.vp-sponsor-grid[data-vp-grid='6'] .vp-sponsor-grid-item { + width: calc((100% - 4px * 5) / 6); +} + +.vp-sponsor-grid-item { + flex-shrink: 0; + width: 100%; + background-color: var(--vp-c-bg-soft); + transition: background-color 0.25s; +} + +.vp-sponsor-grid-item:hover { + background-color: var(--vp-c-default-soft); +} + +.vp-sponsor-grid-item:hover .vp-sponsor-grid-image { + filter: grayscale(0) invert(0); +} + +.vp-sponsor-grid-item.empty:hover { + background-color: var(--vp-c-bg-soft); +} + +.dark .vp-sponsor-grid-item:hover { + background-color: var(--vp-c-white); +} + +.dark .vp-sponsor-grid-item.empty:hover { + background-color: var(--vp-c-bg-soft); +} + +.vp-sponsor-grid-link { + display: flex; +} + +.vp-sponsor-grid-box { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + +.vp-sponsor-grid-image { + max-width: 100%; + filter: grayscale(1); + transition: filter 0.25s; +} + +.dark .vp-sponsor-grid-image { + filter: grayscale(1) invert(1); +} + +.VPBadge { + display: inline-block; + margin-left: 2px; + border: 1px solid transparent; + border-radius: 12px; + padding: 0 10px; + line-height: 22px; + font-size: 12px; + font-weight: 500; + transform: translateY(-2px); +} +.VPBadge.small { + padding: 0 6px; + line-height: 18px; + font-size: 10px; + transform: translateY(-8px); +} +.VPDocFooter .VPBadge { + display: none; +} +.vp-doc h1 > .VPBadge { + margin-top: 4px; + vertical-align: top; +} +.vp-doc h2 > .VPBadge { + margin-top: 3px; + padding: 0 8px; + vertical-align: top; +} +.vp-doc h3 > .VPBadge { + vertical-align: middle; +} +.vp-doc h4 > .VPBadge, +.vp-doc h5 > .VPBadge, +.vp-doc h6 > .VPBadge { + vertical-align: middle; + line-height: 18px; +} +.VPBadge.info { + border-color: var(--vp-badge-info-border); + color: var(--vp-badge-info-text); + background-color: var(--vp-badge-info-bg); +} +.VPBadge.tip { + border-color: var(--vp-badge-tip-border); + color: var(--vp-badge-tip-text); + background-color: var(--vp-badge-tip-bg); +} +.VPBadge.warning { + border-color: var(--vp-badge-warning-border); + color: var(--vp-badge-warning-text); + background-color: var(--vp-badge-warning-bg); +} +.VPBadge.danger { + border-color: var(--vp-badge-danger-border); + color: var(--vp-badge-danger-text); + background-color: var(--vp-badge-danger-bg); +} + +.VPBackdrop[data-v-c79a1216] { + position: fixed; + top: 0; + /*rtl:ignore*/ + right: 0; + bottom: 0; + /*rtl:ignore*/ + left: 0; + z-index: var(--vp-z-index-backdrop); + background: var(--vp-backdrop-bg-color); + transition: opacity 0.5s; +} +.VPBackdrop.fade-enter-from[data-v-c79a1216], +.VPBackdrop.fade-leave-to[data-v-c79a1216] { + opacity: 0; +} +.VPBackdrop.fade-leave-active[data-v-c79a1216] { + transition-duration: .25s; +} +@media (min-width: 1280px) { +.VPBackdrop[data-v-c79a1216] { + display: none; +} +} + +.NotFound[data-v-d6be1790] { + padding: 64px 24px 96px; + text-align: center; +} +@media (min-width: 768px) { +.NotFound[data-v-d6be1790] { + padding: 96px 32px 168px; +} +} +.code[data-v-d6be1790] { + line-height: 64px; + font-size: 64px; + font-weight: 600; +} +.title[data-v-d6be1790] { + padding-top: 12px; + letter-spacing: 2px; + line-height: 20px; + font-size: 20px; + font-weight: 700; +} +.divider[data-v-d6be1790] { + margin: 24px auto 18px; + width: 64px; + height: 1px; + background-color: var(--vp-c-divider); +} +.quote[data-v-d6be1790] { + margin: 0 auto; + max-width: 256px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-2); +} +.action[data-v-d6be1790] { + padding-top: 20px; +} +.link[data-v-d6be1790] { + display: inline-block; + border: 1px solid var(--vp-c-brand-1); + border-radius: 16px; + padding: 3px 16px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-brand-1); + transition: + border-color 0.25s, + color 0.25s; +} +.link[data-v-d6be1790]:hover { + border-color: var(--vp-c-brand-2); + color: var(--vp-c-brand-2); +} + +.root[data-v-b933a997] { + position: relative; + z-index: 1; +} +.nested[data-v-b933a997] { + padding-right: 16px; + padding-left: 16px; +} +.outline-link[data-v-b933a997] { + display: block; + line-height: 32px; + font-size: 14px; + font-weight: 400; + color: var(--vp-c-text-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.5s; +} +.outline-link[data-v-b933a997]:hover, +.outline-link.active[data-v-b933a997] { + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.outline-link.nested[data-v-b933a997] { + padding-left: 13px; +} + +.VPDocAsideOutline[data-v-a5bbad30] { + display: none; +} +.VPDocAsideOutline.has-outline[data-v-a5bbad30] { + display: block; +} +.content[data-v-a5bbad30] { + position: relative; + border-left: 1px solid var(--vp-c-divider); + padding-left: 16px; + font-size: 13px; + font-weight: 500; +} +.outline-marker[data-v-a5bbad30] { + position: absolute; + top: 32px; + left: -1px; + z-index: 0; + opacity: 0; + width: 2px; + border-radius: 2px; + height: 18px; + background-color: var(--vp-c-brand-1); + transition: + top 0.25s cubic-bezier(0, 1, 0.5, 1), + background-color 0.5s, + opacity 0.25s; +} +.outline-title[data-v-a5bbad30] { + line-height: 32px; + font-size: 14px; + font-weight: 600; +} + +.VPDocAside[data-v-3f215769] { + display: flex; + flex-direction: column; + flex-grow: 1; +} +.spacer[data-v-3f215769] { + flex-grow: 1; +} +.VPDocAside[data-v-3f215769] .spacer + .VPDocAsideSponsors, +.VPDocAside[data-v-3f215769] .spacer + .VPDocAsideCarbonAds { + margin-top: 24px; +} +.VPDocAside[data-v-3f215769] .VPDocAsideSponsors + .VPDocAsideCarbonAds { + margin-top: 16px; +} + +.VPLastUpdated[data-v-e98dd255] { + line-height: 24px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-2); +} +@media (min-width: 640px) { +.VPLastUpdated[data-v-e98dd255] { + line-height: 32px; + font-size: 14px; + font-weight: 500; +} +} + +.VPDocFooter[data-v-e257564d] { + margin-top: 64px; +} +.edit-info[data-v-e257564d] { + padding-bottom: 18px; +} +@media (min-width: 640px) { +.edit-info[data-v-e257564d] { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 14px; +} +} +.edit-link-button[data-v-e257564d] { + display: flex; + align-items: center; + border: 0; + line-height: 32px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-brand-1); + transition: color 0.25s; +} +.edit-link-button[data-v-e257564d]:hover { + color: var(--vp-c-brand-2); +} +.edit-link-icon[data-v-e257564d] { + margin-right: 8px; +} +.prev-next[data-v-e257564d] { + border-top: 1px solid var(--vp-c-divider); + padding-top: 24px; + display: grid; + grid-row-gap: 8px; +} +@media (min-width: 640px) { +.prev-next[data-v-e257564d] { + grid-template-columns: repeat(2, 1fr); + grid-column-gap: 16px; +} +} +.pager-link[data-v-e257564d] { + display: block; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + padding: 11px 16px 13px; + width: 100%; + height: 100%; + transition: border-color 0.25s; +} +.pager-link[data-v-e257564d]:hover { + border-color: var(--vp-c-brand-1); +} +.pager-link.next[data-v-e257564d] { + margin-left: auto; + text-align: right; +} +.desc[data-v-e257564d] { + display: block; + line-height: 20px; + font-size: 12px; + font-weight: 500; + color: var(--vp-c-text-2); +} +.title[data-v-e257564d] { + display: block; + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-brand-1); + transition: color 0.25s; +} + +.VPDoc[data-v-39a288b8] { + padding: 32px 24px 96px; + width: 100%; +} +@media (min-width: 768px) { +.VPDoc[data-v-39a288b8] { + padding: 48px 32px 128px; +} +} +@media (min-width: 960px) { +.VPDoc[data-v-39a288b8] { + padding: 48px 32px 0; +} +.VPDoc:not(.has-sidebar) .container[data-v-39a288b8] { + display: flex; + justify-content: center; + max-width: 992px; +} +.VPDoc:not(.has-sidebar) .content[data-v-39a288b8] { + max-width: 752px; +} +} +@media (min-width: 1280px) { +.VPDoc .container[data-v-39a288b8] { + display: flex; + justify-content: center; +} +.VPDoc .aside[data-v-39a288b8] { + display: block; +} +} +@media (min-width: 1440px) { +.VPDoc:not(.has-sidebar) .content[data-v-39a288b8] { + max-width: 784px; +} +.VPDoc:not(.has-sidebar) .container[data-v-39a288b8] { + max-width: 1104px; +} +} +.container[data-v-39a288b8] { + margin: 0 auto; + width: 100%; +} +.aside[data-v-39a288b8] { + position: relative; + display: none; + order: 2; + flex-grow: 1; + padding-left: 32px; + width: 100%; + max-width: 256px; +} +.left-aside[data-v-39a288b8] { + order: 1; + padding-left: unset; + padding-right: 32px; +} +.aside-container[data-v-39a288b8] { + position: fixed; + top: 0; + padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px); + width: 224px; + height: 100vh; + overflow-x: hidden; + overflow-y: auto; + scrollbar-width: none; +} +.aside-container[data-v-39a288b8]::-webkit-scrollbar { + display: none; +} +.aside-curtain[data-v-39a288b8] { + position: fixed; + bottom: 0; + z-index: 10; + width: 224px; + height: 32px; + background: linear-gradient(transparent, var(--vp-c-bg) 70%); +} +.aside-content[data-v-39a288b8] { + display: flex; + flex-direction: column; + min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px)); + padding-bottom: 32px; +} +.content[data-v-39a288b8] { + position: relative; + margin: 0 auto; + width: 100%; +} +@media (min-width: 960px) { +.content[data-v-39a288b8] { + padding: 0 32px 128px; +} +} +@media (min-width: 1280px) { +.content[data-v-39a288b8] { + order: 1; + margin: 0; + min-width: 640px; +} +} +.content-container[data-v-39a288b8] { + margin: 0 auto; +} +.VPDoc.has-aside .content-container[data-v-39a288b8] { + max-width: 688px; +} + +.VPButton[data-v-fa7799d5] { + display: inline-block; + border: 1px solid transparent; + text-align: center; + font-weight: 600; + white-space: nowrap; + transition: color 0.25s, border-color 0.25s, background-color 0.25s; +} +.VPButton[data-v-fa7799d5]:active { + transition: color 0.1s, border-color 0.1s, background-color 0.1s; +} +.VPButton.medium[data-v-fa7799d5] { + border-radius: 20px; + padding: 0 20px; + line-height: 38px; + font-size: 14px; +} +.VPButton.big[data-v-fa7799d5] { + border-radius: 24px; + padding: 0 24px; + line-height: 46px; + font-size: 16px; +} +.VPButton.brand[data-v-fa7799d5] { + border-color: var(--vp-button-brand-border); + color: var(--vp-button-brand-text); + background-color: var(--vp-button-brand-bg); +} +.VPButton.brand[data-v-fa7799d5]:hover { + border-color: var(--vp-button-brand-hover-border); + color: var(--vp-button-brand-hover-text); + background-color: var(--vp-button-brand-hover-bg); +} +.VPButton.brand[data-v-fa7799d5]:active { + border-color: var(--vp-button-brand-active-border); + color: var(--vp-button-brand-active-text); + background-color: var(--vp-button-brand-active-bg); +} +.VPButton.alt[data-v-fa7799d5] { + border-color: var(--vp-button-alt-border); + color: var(--vp-button-alt-text); + background-color: var(--vp-button-alt-bg); +} +.VPButton.alt[data-v-fa7799d5]:hover { + border-color: var(--vp-button-alt-hover-border); + color: var(--vp-button-alt-hover-text); + background-color: var(--vp-button-alt-hover-bg); +} +.VPButton.alt[data-v-fa7799d5]:active { + border-color: var(--vp-button-alt-active-border); + color: var(--vp-button-alt-active-text); + background-color: var(--vp-button-alt-active-bg); +} +.VPButton.sponsor[data-v-fa7799d5] { + border-color: var(--vp-button-sponsor-border); + color: var(--vp-button-sponsor-text); + background-color: var(--vp-button-sponsor-bg); +} +.VPButton.sponsor[data-v-fa7799d5]:hover { + border-color: var(--vp-button-sponsor-hover-border); + color: var(--vp-button-sponsor-hover-text); + background-color: var(--vp-button-sponsor-hover-bg); +} +.VPButton.sponsor[data-v-fa7799d5]:active { + border-color: var(--vp-button-sponsor-active-border); + color: var(--vp-button-sponsor-active-text); + background-color: var(--vp-button-sponsor-active-bg); +} + +html:not(.dark) .VPImage.dark[data-v-8426fc1a] { + display: none; +} +.dark .VPImage.light[data-v-8426fc1a] { + display: none; +} + +.VPHero[data-v-4f9c455b] { + margin-top: calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1); + padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px; +} +@media (min-width: 640px) { +.VPHero[data-v-4f9c455b] { + padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px; +} +} +@media (min-width: 960px) { +.VPHero[data-v-4f9c455b] { + padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px; +} +} +.container[data-v-4f9c455b] { + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 1152px; +} +@media (min-width: 960px) { +.container[data-v-4f9c455b] { + flex-direction: row; +} +} +.main[data-v-4f9c455b] { + position: relative; + z-index: 10; + order: 2; + flex-grow: 1; + flex-shrink: 0; +} +.VPHero.has-image .container[data-v-4f9c455b] { + text-align: center; +} +@media (min-width: 960px) { +.VPHero.has-image .container[data-v-4f9c455b] { + text-align: left; +} +} +@media (min-width: 960px) { +.main[data-v-4f9c455b] { + order: 1; + width: calc((100% / 3) * 2); +} +.VPHero.has-image .main[data-v-4f9c455b] { + max-width: 592px; +} +} +.heading[data-v-4f9c455b] { + display: flex; + flex-direction: column; +} +.name[data-v-4f9c455b], +.text[data-v-4f9c455b] { + width: fit-content; + max-width: 392px; + letter-spacing: -0.4px; + line-height: 40px; + font-size: 32px; + font-weight: 700; + white-space: pre-wrap; +} +.VPHero.has-image .name[data-v-4f9c455b], +.VPHero.has-image .text[data-v-4f9c455b] { + margin: 0 auto; +} +.name[data-v-4f9c455b] { + color: var(--vp-home-hero-name-color); +} +.clip[data-v-4f9c455b] { + background: var(--vp-home-hero-name-background); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: var(--vp-home-hero-name-color); +} +@media (min-width: 640px) { +.name[data-v-4f9c455b], + .text[data-v-4f9c455b] { + max-width: 576px; + line-height: 56px; + font-size: 48px; +} +} +@media (min-width: 960px) { +.name[data-v-4f9c455b], + .text[data-v-4f9c455b] { + line-height: 64px; + font-size: 56px; +} +.VPHero.has-image .name[data-v-4f9c455b], + .VPHero.has-image .text[data-v-4f9c455b] { + margin: 0; +} +} +.tagline[data-v-4f9c455b] { + padding-top: 8px; + max-width: 392px; + line-height: 28px; + font-size: 18px; + font-weight: 500; + white-space: pre-wrap; + color: var(--vp-c-text-2); +} +.VPHero.has-image .tagline[data-v-4f9c455b] { + margin: 0 auto; +} +@media (min-width: 640px) { +.tagline[data-v-4f9c455b] { + padding-top: 12px; + max-width: 576px; + line-height: 32px; + font-size: 20px; +} +} +@media (min-width: 960px) { +.tagline[data-v-4f9c455b] { + line-height: 36px; + font-size: 24px; +} +.VPHero.has-image .tagline[data-v-4f9c455b] { + margin: 0; +} +} +.actions[data-v-4f9c455b] { + display: flex; + flex-wrap: wrap; + margin: -6px; + padding-top: 24px; +} +.VPHero.has-image .actions[data-v-4f9c455b] { + justify-content: center; +} +@media (min-width: 640px) { +.actions[data-v-4f9c455b] { + padding-top: 32px; +} +} +@media (min-width: 960px) { +.VPHero.has-image .actions[data-v-4f9c455b] { + justify-content: flex-start; +} +} +.action[data-v-4f9c455b] { + flex-shrink: 0; + padding: 6px; +} +.image[data-v-4f9c455b] { + order: 1; + margin: -76px -24px -48px; +} +@media (min-width: 640px) { +.image[data-v-4f9c455b] { + margin: -108px -24px -48px; +} +} +@media (min-width: 960px) { +.image[data-v-4f9c455b] { + flex-grow: 1; + order: 2; + margin: 0; + min-height: 100%; +} +} +.image-container[data-v-4f9c455b] { + position: relative; + margin: 0 auto; + width: 320px; + height: 320px; +} +@media (min-width: 640px) { +.image-container[data-v-4f9c455b] { + width: 392px; + height: 392px; +} +} +@media (min-width: 960px) { +.image-container[data-v-4f9c455b] { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + /*rtl:ignore*/ + transform: translate(-32px, -32px); +} +} +.image-bg[data-v-4f9c455b] { + position: absolute; + top: 50%; + /*rtl:ignore*/ + left: 50%; + border-radius: 50%; + width: 192px; + height: 192px; + background-image: var(--vp-home-hero-image-background-image); + filter: var(--vp-home-hero-image-filter); + /*rtl:ignore*/ + transform: translate(-50%, -50%); +} +@media (min-width: 640px) { +.image-bg[data-v-4f9c455b] { + width: 256px; + height: 256px; +} +} +@media (min-width: 960px) { +.image-bg[data-v-4f9c455b] { + width: 320px; + height: 320px; +} +} +[data-v-4f9c455b] .image-src { + position: absolute; + top: 50%; + /*rtl:ignore*/ + left: 50%; + max-width: 192px; + max-height: 192px; + /*rtl:ignore*/ + transform: translate(-50%, -50%); +} +@media (min-width: 640px) { +[data-v-4f9c455b] .image-src { + max-width: 256px; + max-height: 256px; +} +} +@media (min-width: 960px) { +[data-v-4f9c455b] .image-src { + max-width: 320px; + max-height: 320px; +} +} + +.VPFeature[data-v-a3976bdc] { + display: block; + border: 1px solid var(--vp-c-bg-soft); + border-radius: 12px; + height: 100%; + background-color: var(--vp-c-bg-soft); + transition: border-color 0.25s, background-color 0.25s; +} +.VPFeature.link[data-v-a3976bdc]:hover { + border-color: var(--vp-c-brand-1); +} +.box[data-v-a3976bdc] { + display: flex; + flex-direction: column; + padding: 24px; + height: 100%; +} +.box[data-v-a3976bdc] > .VPImage { + margin-bottom: 20px; +} +.icon[data-v-a3976bdc] { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; + border-radius: 6px; + background-color: var(--vp-c-default-soft); + width: 48px; + height: 48px; + font-size: 24px; + transition: background-color 0.25s; +} +.title[data-v-a3976bdc] { + line-height: 24px; + font-size: 16px; + font-weight: 600; +} +.details[data-v-a3976bdc] { + flex-grow: 1; + padding-top: 8px; + line-height: 24px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-2); +} +.link-text[data-v-a3976bdc] { + padding-top: 8px; +} +.link-text-value[data-v-a3976bdc] { + display: flex; + align-items: center; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-brand-1); +} +.link-text-icon[data-v-a3976bdc] { + margin-left: 6px; +} + +.VPFeatures[data-v-a6181336] { + position: relative; + padding: 0 24px; +} +@media (min-width: 640px) { +.VPFeatures[data-v-a6181336] { + padding: 0 48px; +} +} +@media (min-width: 960px) { +.VPFeatures[data-v-a6181336] { + padding: 0 64px; +} +} +.container[data-v-a6181336] { + margin: 0 auto; + max-width: 1152px; +} +.items[data-v-a6181336] { + display: flex; + flex-wrap: wrap; + margin: -8px; +} +.item[data-v-a6181336] { + padding: 8px; + width: 100%; +} +@media (min-width: 640px) { +.item.grid-2[data-v-a6181336], + .item.grid-4[data-v-a6181336], + .item.grid-6[data-v-a6181336] { + width: calc(100% / 2); +} +} +@media (min-width: 768px) { +.item.grid-2[data-v-a6181336], + .item.grid-4[data-v-a6181336] { + width: calc(100% / 2); +} +.item.grid-3[data-v-a6181336], + .item.grid-6[data-v-a6181336] { + width: calc(100% / 3); +} +} +@media (min-width: 960px) { +.item.grid-4[data-v-a6181336] { + width: calc(100% / 4); +} +} + +.container[data-v-8e2d4988] { + margin: auto; + width: 100%; + max-width: 1280px; + padding: 0 24px; +} +@media (min-width: 640px) { +.container[data-v-8e2d4988] { + padding: 0 48px; +} +} +@media (min-width: 960px) { +.container[data-v-8e2d4988] { + width: 100%; + padding: 0 64px; +} +} +.vp-doc[data-v-8e2d4988] .VPHomeSponsors, +.vp-doc[data-v-8e2d4988] .VPTeamPage { + margin-left: var(--vp-offset, calc(50% - 50vw)); + margin-right: var(--vp-offset, calc(50% - 50vw)); +} +.vp-doc[data-v-8e2d4988] .VPHomeSponsors h2 { + border-top: none; + letter-spacing: normal; +} +.vp-doc[data-v-8e2d4988] .VPHomeSponsors a, +.vp-doc[data-v-8e2d4988] .VPTeamPage a { + text-decoration: none; +} + +.VPHome[data-v-8b561e3d] { + margin-bottom: 96px; +} +@media (min-width: 768px) { +.VPHome[data-v-8b561e3d] { + margin-bottom: 128px; +} +} + +.VPContent[data-v-1428d186] { + flex-grow: 1; + flex-shrink: 0; + margin: var(--vp-layout-top-height, 0px) auto 0; + width: 100%; +} +.VPContent.is-home[data-v-1428d186] { + width: 100%; + max-width: 100%; +} +.VPContent.has-sidebar[data-v-1428d186] { + margin: 0; +} +@media (min-width: 960px) { +.VPContent[data-v-1428d186] { + padding-top: var(--vp-nav-height); +} +.VPContent.has-sidebar[data-v-1428d186] { + margin: var(--vp-layout-top-height, 0px) 0 0; + padding-left: var(--vp-sidebar-width); +} +} +@media (min-width: 1440px) { +.VPContent.has-sidebar[data-v-1428d186] { + padding-right: calc((100vw - var(--vp-layout-max-width)) / 2); + padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)); +} +} + +.VPFooter[data-v-e315a0ad] { + position: relative; + z-index: var(--vp-z-index-footer); + border-top: 1px solid var(--vp-c-gutter); + padding: 32px 24px; + background-color: var(--vp-c-bg); +} +.VPFooter.has-sidebar[data-v-e315a0ad] { + display: none; +} +.VPFooter[data-v-e315a0ad] a { + text-decoration-line: underline; + text-underline-offset: 2px; + transition: color 0.25s; +} +.VPFooter[data-v-e315a0ad] a:hover { + color: var(--vp-c-text-1); +} +@media (min-width: 768px) { +.VPFooter[data-v-e315a0ad] { + padding: 32px; +} +} +.container[data-v-e315a0ad] { + margin: 0 auto; + max-width: var(--vp-layout-max-width); + text-align: center; +} +.message[data-v-e315a0ad], +.copyright[data-v-e315a0ad] { + line-height: 24px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-2); +} + +.VPLocalNavOutlineDropdown[data-v-8a42e2b4] { + padding: 12px 20px 11px; +} +@media (min-width: 960px) { +.VPLocalNavOutlineDropdown[data-v-8a42e2b4] { + padding: 12px 36px 11px; +} +} +.VPLocalNavOutlineDropdown button[data-v-8a42e2b4] { + display: block; + font-size: 12px; + font-weight: 500; + line-height: 24px; + color: var(--vp-c-text-2); + transition: color 0.5s; + position: relative; +} +.VPLocalNavOutlineDropdown button[data-v-8a42e2b4]:hover { + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.VPLocalNavOutlineDropdown button.open[data-v-8a42e2b4] { + color: var(--vp-c-text-1); +} +.icon[data-v-8a42e2b4] { + display: inline-block; + vertical-align: middle; + margin-left: 2px; + font-size: 14px; + transform: rotate(0)/*rtl:rotate(180deg)*/; + transition: transform 0.25s; +} +@media (min-width: 960px) { +.VPLocalNavOutlineDropdown button[data-v-8a42e2b4] { + font-size: 14px; +} +.icon[data-v-8a42e2b4] { + font-size: 16px; +} +} +.open > .icon[data-v-8a42e2b4] { + /*rtl:ignore*/ + transform: rotate(90deg); +} +.items[data-v-8a42e2b4] { + position: absolute; + top: 40px; + right: 16px; + left: 16px; + display: grid; + gap: 1px; + border: 1px solid var(--vp-c-border); + border-radius: 8px; + background-color: var(--vp-c-gutter); + max-height: calc(var(--vp-vh, 100vh) - 86px); + overflow: hidden auto; + box-shadow: var(--vp-shadow-3); +} +@media (min-width: 960px) { +.items[data-v-8a42e2b4] { + right: auto; + left: calc(var(--vp-sidebar-width) + 32px); + width: 320px; +} +} +.header[data-v-8a42e2b4] { + background-color: var(--vp-c-bg-soft); +} +.top-link[data-v-8a42e2b4] { + display: block; + padding: 0 16px; + line-height: 48px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-brand-1); +} +.outline[data-v-8a42e2b4] { + padding: 8px 0; + background-color: var(--vp-c-bg-soft); +} +.flyout-enter-active[data-v-8a42e2b4] { + transition: all 0.2s ease-out; +} +.flyout-leave-active[data-v-8a42e2b4] { + transition: all 0.15s ease-in; +} +.flyout-enter-from[data-v-8a42e2b4], +.flyout-leave-to[data-v-8a42e2b4] { + opacity: 0; + transform: translateY(-16px); +} + +.VPLocalNav[data-v-a6f0e41e] { + position: sticky; + top: 0; + /*rtl:ignore*/ + left: 0; + z-index: var(--vp-z-index-local-nav); + border-bottom: 1px solid var(--vp-c-gutter); + padding-top: var(--vp-layout-top-height, 0px); + width: 100%; + background-color: var(--vp-local-nav-bg-color); +} +.VPLocalNav.fixed[data-v-a6f0e41e] { + position: fixed; +} +@media (min-width: 960px) { +.VPLocalNav[data-v-a6f0e41e] { + top: var(--vp-nav-height); +} +.VPLocalNav.has-sidebar[data-v-a6f0e41e] { + padding-left: var(--vp-sidebar-width); +} +.VPLocalNav.empty[data-v-a6f0e41e] { + display: none; +} +} +@media (min-width: 1280px) { +.VPLocalNav[data-v-a6f0e41e] { + display: none; +} +} +@media (min-width: 1440px) { +.VPLocalNav.has-sidebar[data-v-a6f0e41e] { + padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)); +} +} +.container[data-v-a6f0e41e] { + display: flex; + justify-content: space-between; + align-items: center; +} +.menu[data-v-a6f0e41e] { + display: flex; + align-items: center; + padding: 12px 24px 11px; + line-height: 24px; + font-size: 12px; + font-weight: 500; + color: var(--vp-c-text-2); + transition: color 0.5s; +} +.menu[data-v-a6f0e41e]:hover { + color: var(--vp-c-text-1); + transition: color 0.25s; +} +@media (min-width: 768px) { +.menu[data-v-a6f0e41e] { + padding: 0 32px; +} +} +@media (min-width: 960px) { +.menu[data-v-a6f0e41e] { + display: none; +} +} +.menu-icon[data-v-a6f0e41e] { + margin-right: 8px; + font-size: 14px; +} +.VPOutlineDropdown[data-v-a6f0e41e] { + padding: 12px 24px 11px; +} +@media (min-width: 768px) { +.VPOutlineDropdown[data-v-a6f0e41e] { + padding: 12px 32px 11px; +} +} + +.VPSwitch[data-v-1d5665e3] { + position: relative; + border-radius: 11px; + display: block; + width: 40px; + height: 22px; + flex-shrink: 0; + border: 1px solid var(--vp-input-border-color); + background-color: var(--vp-input-switch-bg-color); + transition: border-color 0.25s !important; +} +.VPSwitch[data-v-1d5665e3]:hover { + border-color: var(--vp-c-brand-1); +} +.check[data-v-1d5665e3] { + position: absolute; + top: 1px; + /*rtl:ignore*/ + left: 1px; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: var(--vp-c-neutral-inverse); + box-shadow: var(--vp-shadow-1); + transition: transform 0.25s !important; +} +.icon[data-v-1d5665e3] { + position: relative; + display: block; + width: 18px; + height: 18px; + border-radius: 50%; + overflow: hidden; +} +.icon[data-v-1d5665e3] [class^='vpi-'] { + position: absolute; + top: 3px; + left: 3px; + width: 12px; + height: 12px; + color: var(--vp-c-text-2); +} +.dark .icon[data-v-1d5665e3] [class^='vpi-'] { + color: var(--vp-c-text-1); + transition: opacity 0.25s !important; +} + +.sun[data-v-5337faa4] { + opacity: 1; +} +.moon[data-v-5337faa4] { + opacity: 0; +} +.dark .sun[data-v-5337faa4] { + opacity: 0; +} +.dark .moon[data-v-5337faa4] { + opacity: 1; +} +.dark .VPSwitchAppearance[data-v-5337faa4] .check { + /*rtl:ignore*/ + transform: translateX(18px); +} + +.VPNavBarAppearance[data-v-6c893767] { + display: none; +} +@media (min-width: 1280px) { +.VPNavBarAppearance[data-v-6c893767] { + display: flex; + align-items: center; +} +} + +.VPMenuGroup + .VPMenuLink[data-v-35975db6] { + margin: 12px -12px 0; + border-top: 1px solid var(--vp-c-divider); + padding: 12px 12px 0; +} +.link[data-v-35975db6] { + display: block; + border-radius: 6px; + padding: 0 12px; + line-height: 32px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); + white-space: nowrap; + transition: + background-color 0.25s, + color 0.25s; +} +.link[data-v-35975db6]:hover { + color: var(--vp-c-brand-1); + background-color: var(--vp-c-default-soft); +} +.link.active[data-v-35975db6] { + color: var(--vp-c-brand-1); +} + +.VPMenuGroup[data-v-69e747b5] { + margin: 12px -12px 0; + border-top: 1px solid var(--vp-c-divider); + padding: 12px 12px 0; +} +.VPMenuGroup[data-v-69e747b5]:first-child { + margin-top: 0; + border-top: 0; + padding-top: 0; +} +.VPMenuGroup + .VPMenuGroup[data-v-69e747b5] { + margin-top: 12px; + border-top: 1px solid var(--vp-c-divider); +} +.title[data-v-69e747b5] { + padding: 0 12px; + line-height: 32px; + font-size: 14px; + font-weight: 600; + color: var(--vp-c-text-2); + white-space: nowrap; + transition: color 0.25s; +} + +.VPMenu[data-v-b98bc113] { + border-radius: 12px; + padding: 12px; + min-width: 128px; + border: 1px solid var(--vp-c-divider); + background-color: var(--vp-c-bg-elv); + box-shadow: var(--vp-shadow-3); + transition: background-color 0.5s; + max-height: calc(100vh - var(--vp-nav-height)); + overflow-y: auto; +} +.VPMenu[data-v-b98bc113] .group { + margin: 0 -12px; + padding: 0 12px 12px; +} +.VPMenu[data-v-b98bc113] .group + .group { + border-top: 1px solid var(--vp-c-divider); + padding: 11px 12px 12px; +} +.VPMenu[data-v-b98bc113] .group:last-child { + padding-bottom: 0; +} +.VPMenu[data-v-b98bc113] .group + .item { + border-top: 1px solid var(--vp-c-divider); + padding: 11px 16px 0; +} +.VPMenu[data-v-b98bc113] .item { + padding: 0 16px; + white-space: nowrap; +} +.VPMenu[data-v-b98bc113] .label { + flex-grow: 1; + line-height: 28px; + font-size: 12px; + font-weight: 500; + color: var(--vp-c-text-2); + transition: color 0.5s; +} +.VPMenu[data-v-b98bc113] .action { + padding-left: 24px; +} + +.VPFlyout[data-v-cf11d7a2] { + position: relative; +} +.VPFlyout[data-v-cf11d7a2]:hover { + color: var(--vp-c-brand-1); + transition: color 0.25s; +} +.VPFlyout:hover .text[data-v-cf11d7a2] { + color: var(--vp-c-text-2); +} +.VPFlyout:hover .icon[data-v-cf11d7a2] { + fill: var(--vp-c-text-2); +} +.VPFlyout.active .text[data-v-cf11d7a2] { + color: var(--vp-c-brand-1); +} +.VPFlyout.active:hover .text[data-v-cf11d7a2] { + color: var(--vp-c-brand-2); +} +.button[aria-expanded="false"] + .menu[data-v-cf11d7a2] { + opacity: 0; + visibility: hidden; + transform: translateY(0); +} +.VPFlyout:hover .menu[data-v-cf11d7a2], +.button[aria-expanded="true"] + .menu[data-v-cf11d7a2] { + opacity: 1; + visibility: visible; + transform: translateY(0); +} +.button[data-v-cf11d7a2] { + display: flex; + align-items: center; + padding: 0 12px; + height: var(--vp-nav-height); + color: var(--vp-c-text-1); + transition: color 0.5s; +} +.text[data-v-cf11d7a2] { + display: flex; + align-items: center; + line-height: var(--vp-nav-height); + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.option-icon[data-v-cf11d7a2] { + margin-right: 0px; + font-size: 16px; +} +.text-icon[data-v-cf11d7a2] { + margin-left: 4px; + font-size: 14px; +} +.icon[data-v-cf11d7a2] { + font-size: 20px; + transition: fill 0.25s; +} +.menu[data-v-cf11d7a2] { + position: absolute; + top: calc(var(--vp-nav-height) / 2 + 20px); + right: 0; + opacity: 0; + visibility: hidden; + transition: opacity 0.25s, visibility 0.25s, transform 0.25s; +} + +.VPSocialLink[data-v-bd121fe5] { + display: flex; + justify-content: center; + align-items: center; + width: 36px; + height: 36px; + color: var(--vp-c-text-2); + transition: color 0.5s; +} +.VPSocialLink[data-v-bd121fe5]:hover { + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.VPSocialLink[data-v-bd121fe5] > svg, +.VPSocialLink[data-v-bd121fe5] > [class^="vpi-social-"] { + width: 20px; + height: 20px; + fill: currentColor; +} + +.VPSocialLinks[data-v-7bc22406] { + display: flex; + justify-content: center; +} + +.VPNavBarExtra[data-v-bb2aa2f0] { + display: none; + margin-right: -12px; +} +@media (min-width: 768px) { +.VPNavBarExtra[data-v-bb2aa2f0] { + display: block; +} +} +@media (min-width: 1280px) { +.VPNavBarExtra[data-v-bb2aa2f0] { + display: none; +} +} +.trans-title[data-v-bb2aa2f0] { + padding: 0 24px 0 12px; + line-height: 32px; + font-size: 14px; + font-weight: 700; + color: var(--vp-c-text-1); +} +.item.appearance[data-v-bb2aa2f0], +.item.social-links[data-v-bb2aa2f0] { + display: flex; + align-items: center; + padding: 0 12px; +} +.item.appearance[data-v-bb2aa2f0] { + min-width: 176px; +} +.appearance-action[data-v-bb2aa2f0] { + margin-right: -2px; +} +.social-links-list[data-v-bb2aa2f0] { + margin: -4px -8px; +} + +.VPNavBarHamburger[data-v-e5dd9c1c] { + display: flex; + justify-content: center; + align-items: center; + width: 48px; + height: var(--vp-nav-height); +} +@media (min-width: 768px) { +.VPNavBarHamburger[data-v-e5dd9c1c] { + display: none; +} +} +.container[data-v-e5dd9c1c] { + position: relative; + width: 16px; + height: 14px; + overflow: hidden; +} +.VPNavBarHamburger:hover .top[data-v-e5dd9c1c] { top: 0; left: 0; transform: translateX(4px); +} +.VPNavBarHamburger:hover .middle[data-v-e5dd9c1c] { top: 6px; left: 0; transform: translateX(0); +} +.VPNavBarHamburger:hover .bottom[data-v-e5dd9c1c] { top: 12px; left: 0; transform: translateX(8px); +} +.VPNavBarHamburger.active .top[data-v-e5dd9c1c] { top: 6px; transform: translateX(0) rotate(225deg); +} +.VPNavBarHamburger.active .middle[data-v-e5dd9c1c] { top: 6px; transform: translateX(16px); +} +.VPNavBarHamburger.active .bottom[data-v-e5dd9c1c] { top: 6px; transform: translateX(0) rotate(135deg); +} +.VPNavBarHamburger.active:hover .top[data-v-e5dd9c1c], +.VPNavBarHamburger.active:hover .middle[data-v-e5dd9c1c], +.VPNavBarHamburger.active:hover .bottom[data-v-e5dd9c1c] { + background-color: var(--vp-c-text-2); + transition: top .25s, background-color .25s, transform .25s; +} +.top[data-v-e5dd9c1c], +.middle[data-v-e5dd9c1c], +.bottom[data-v-e5dd9c1c] { + position: absolute; + width: 16px; + height: 2px; + background-color: var(--vp-c-text-1); + transition: top .25s, background-color .5s, transform .25s; +} +.top[data-v-e5dd9c1c] { top: 0; left: 0; transform: translateX(0); +} +.middle[data-v-e5dd9c1c] { top: 6px; left: 0; transform: translateX(8px); +} +.bottom[data-v-e5dd9c1c] { top: 12px; left: 0; transform: translateX(4px); +} + +.VPNavBarMenuLink[data-v-e56f3d57] { + display: flex; + align-items: center; + padding: 0 12px; + line-height: var(--vp-nav-height); + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.VPNavBarMenuLink.active[data-v-e56f3d57] { + color: var(--vp-c-brand-1); +} +.VPNavBarMenuLink[data-v-e56f3d57]:hover { + color: var(--vp-c-brand-1); +} + +.VPNavBarMenu[data-v-dc692963] { + display: none; +} +@media (min-width: 768px) { +.VPNavBarMenu[data-v-dc692963] { + display: flex; +} +} +/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */ +:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}} +[class*='DocSearch'] { + --docsearch-primary-color: var(--vp-c-brand-1); + --docsearch-highlight-color: var(--docsearch-primary-color); + --docsearch-text-color: var(--vp-c-text-1); + --docsearch-muted-color: var(--vp-c-text-2); + --docsearch-searchbox-shadow: none; + --docsearch-searchbox-background: transparent; + --docsearch-searchbox-focus-background: transparent; + --docsearch-key-gradient: transparent; + --docsearch-key-shadow: none; + --docsearch-modal-background: var(--vp-c-bg-soft); + --docsearch-footer-background: var(--vp-c-bg); +} +.dark [class*='DocSearch'] { + --docsearch-modal-shadow: none; + --docsearch-footer-shadow: none; + --docsearch-logo-color: var(--vp-c-text-2); + --docsearch-hit-background: var(--vp-c-default-soft); + --docsearch-hit-color: var(--vp-c-text-2); + --docsearch-hit-shadow: none; +} +.DocSearch-Button { + display: flex; + justify-content: center; + align-items: center; + margin: 0; + padding: 0; + width: 48px; + height: 55px; + background: transparent; + transition: border-color 0.25s; +} +.DocSearch-Button:hover { + background: transparent; +} +.DocSearch-Button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} +.DocSearch-Button-Key--pressed { + transform: none; + box-shadow: none; +} +.DocSearch-Button:focus:not(:focus-visible) { + outline: none !important; +} +@media (min-width: 768px) { +.DocSearch-Button { + justify-content: flex-start; + border: 1px solid transparent; + border-radius: 8px; + padding: 0 10px 0 12px; + width: 100%; + height: 40px; + background-color: var(--vp-c-bg-alt); +} +.DocSearch-Button:hover { + border-color: var(--vp-c-brand-1); + background: var(--vp-c-bg-alt); +} +} +.DocSearch-Button .DocSearch-Button-Container { + display: flex; + align-items: center; +} +.DocSearch-Button .DocSearch-Search-Icon { + position: relative; + width: 16px; + height: 16px; + color: var(--vp-c-text-1); + fill: currentColor; + transition: color 0.5s; +} +.DocSearch-Button:hover .DocSearch-Search-Icon { + color: var(--vp-c-text-1); +} +@media (min-width: 768px) { +.DocSearch-Button .DocSearch-Search-Icon { + top: 1px; + margin-right: 8px; + width: 14px; + height: 14px; + color: var(--vp-c-text-2); +} +} +.DocSearch-Button .DocSearch-Button-Placeholder { + display: none; + margin-top: 2px; + padding: 0 16px 0 0; + font-size: 13px; + font-weight: 500; + color: var(--vp-c-text-2); + transition: color 0.5s; +} +.DocSearch-Button:hover .DocSearch-Button-Placeholder { + color: var(--vp-c-text-1); +} +@media (min-width: 768px) { +.DocSearch-Button .DocSearch-Button-Placeholder { + display: inline-block; +} +} +.DocSearch-Button .DocSearch-Button-Keys { + /*rtl:ignore*/ + direction: ltr; + display: none; + min-width: auto; +} +@media (min-width: 768px) { +.DocSearch-Button .DocSearch-Button-Keys { + display: flex; + align-items: center; +} +} +.DocSearch-Button .DocSearch-Button-Key { + display: block; + margin: 2px 0 0 0; + border: 1px solid var(--vp-c-divider); + /*rtl:begin:ignore*/ + border-right: none; + border-radius: 4px 0 0 4px; + padding-left: 6px; + /*rtl:end:ignore*/ + min-width: 0; + width: auto; + height: 22px; + line-height: 22px; + font-family: var(--vp-font-family-base); + font-size: 12px; + font-weight: 500; + transition: color 0.5s, border-color 0.5s; +} +.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key { + /*rtl:begin:ignore*/ + border-right: 1px solid var(--vp-c-divider); + border-left: none; + border-radius: 0 4px 4px 0; + padding-left: 2px; + padding-right: 6px; + /*rtl:end:ignore*/ +} +.DocSearch-Button .DocSearch-Button-Key:first-child { + font-size: 0 !important; +} +.DocSearch-Button .DocSearch-Button-Key:first-child:after { + content: 'Ctrl'; + font-size: 12px; + letter-spacing: normal; + color: var(--docsearch-muted-color); +} +.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after { + content: '\2318'; +} +.DocSearch-Button .DocSearch-Button-Key:first-child > * { + display: none; +} +.DocSearch-Search-Icon { + --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E"); +} + +.VPNavBarSearch { + display: flex; + align-items: center; +} +@media (min-width: 768px) { +.VPNavBarSearch { + flex-grow: 1; + padding-left: 24px; +} +} +@media (min-width: 960px) { +.VPNavBarSearch { + padding-left: 32px; +} +} +.dark .DocSearch-Footer { + border-top: 1px solid var(--vp-c-divider); +} +.DocSearch-Form { + border: 1px solid var(--vp-c-brand-1); + background-color: var(--vp-c-white); +} +.dark .DocSearch-Form { + background-color: var(--vp-c-default-soft); +} +.DocSearch-Screen-Icon > svg { + margin: auto; +} + +.VPNavBarSocialLinks[data-v-0394ad82] { + display: none; +} +@media (min-width: 1280px) { +.VPNavBarSocialLinks[data-v-0394ad82] { + display: flex; + align-items: center; +} +} + +.title[data-v-1168a8e4] { + display: flex; + align-items: center; + border-bottom: 1px solid transparent; + width: 100%; + height: var(--vp-nav-height); + font-size: 16px; + font-weight: 600; + color: var(--vp-c-text-1); + transition: opacity 0.25s; +} +@media (min-width: 960px) { +.title[data-v-1168a8e4] { + flex-shrink: 0; +} +.VPNavBarTitle.has-sidebar .title[data-v-1168a8e4] { + border-bottom-color: var(--vp-c-divider); +} +} +[data-v-1168a8e4] .logo { + margin-right: 8px; + height: var(--vp-nav-logo-height); +} + +.VPNavBarTranslations[data-v-88af2de4] { + display: none; +} +@media (min-width: 1280px) { +.VPNavBarTranslations[data-v-88af2de4] { + display: flex; + align-items: center; +} +} +.title[data-v-88af2de4] { + padding: 0 24px 0 12px; + line-height: 32px; + font-size: 14px; + font-weight: 700; + color: var(--vp-c-text-1); +} + +.VPNavBar[data-v-6aa21345] { + position: relative; + height: var(--vp-nav-height); + pointer-events: none; + white-space: nowrap; + transition: background-color 0.25s; +} +.VPNavBar.screen-open[data-v-6aa21345] { + transition: none; + background-color: var(--vp-nav-bg-color); + border-bottom: 1px solid var(--vp-c-divider); +} +.VPNavBar[data-v-6aa21345]:not(.home) { + background-color: var(--vp-nav-bg-color); +} +@media (min-width: 960px) { +.VPNavBar[data-v-6aa21345]:not(.home) { + background-color: transparent; +} +.VPNavBar[data-v-6aa21345]:not(.has-sidebar):not(.home.top) { + background-color: var(--vp-nav-bg-color); +} +} +.wrapper[data-v-6aa21345] { + padding: 0 8px 0 24px; +} +@media (min-width: 768px) { +.wrapper[data-v-6aa21345] { + padding: 0 32px; +} +} +@media (min-width: 960px) { +.VPNavBar.has-sidebar .wrapper[data-v-6aa21345] { + padding: 0; +} +} +.container[data-v-6aa21345] { + display: flex; + justify-content: space-between; + margin: 0 auto; + max-width: calc(var(--vp-layout-max-width) - 64px); + height: var(--vp-nav-height); + pointer-events: none; +} +.container > .title[data-v-6aa21345], +.container > .content[data-v-6aa21345] { + pointer-events: none; +} +.container[data-v-6aa21345] * { + pointer-events: auto; +} +@media (min-width: 960px) { +.VPNavBar.has-sidebar .container[data-v-6aa21345] { + max-width: 100%; +} +} +.title[data-v-6aa21345] { + flex-shrink: 0; + height: calc(var(--vp-nav-height) - 1px); + transition: background-color 0.5s; +} +@media (min-width: 960px) { +.VPNavBar.has-sidebar .title[data-v-6aa21345] { + position: absolute; + top: 0; + left: 0; + z-index: 2; + padding: 0 32px; + width: var(--vp-sidebar-width); + height: var(--vp-nav-height); + background-color: transparent; +} +} +@media (min-width: 1440px) { +.VPNavBar.has-sidebar .title[data-v-6aa21345] { + padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2)); + width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px); +} +} +.content[data-v-6aa21345] { + flex-grow: 1; +} +@media (min-width: 960px) { +.VPNavBar.has-sidebar .content[data-v-6aa21345] { + position: relative; + z-index: 1; + padding-right: 32px; + padding-left: var(--vp-sidebar-width); +} +} +@media (min-width: 1440px) { +.VPNavBar.has-sidebar .content[data-v-6aa21345] { + padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px); + padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)); +} +} +.content-body[data-v-6aa21345] { + display: flex; + justify-content: flex-end; + align-items: center; + height: var(--vp-nav-height); + transition: background-color 0.5s; +} +@media (min-width: 960px) { +.VPNavBar:not(.home.top) .content-body[data-v-6aa21345] { + position: relative; + background-color: var(--vp-nav-bg-color); +} +.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-6aa21345] { + background-color: transparent; +} +} +@media (max-width: 767px) { +.content-body[data-v-6aa21345] { + column-gap: 0.5rem; +} +} +.menu + .translations[data-v-6aa21345]::before, +.menu + .appearance[data-v-6aa21345]::before, +.menu + .social-links[data-v-6aa21345]::before, +.translations + .appearance[data-v-6aa21345]::before, +.appearance + .social-links[data-v-6aa21345]::before { + margin-right: 8px; + margin-left: 8px; + width: 1px; + height: 24px; + background-color: var(--vp-c-divider); + content: ""; +} +.menu + .appearance[data-v-6aa21345]::before, +.translations + .appearance[data-v-6aa21345]::before { + margin-right: 16px; +} +.appearance + .social-links[data-v-6aa21345]::before { + margin-left: 16px; +} +.social-links[data-v-6aa21345] { + margin-right: -8px; +} +.divider[data-v-6aa21345] { + width: 100%; + height: 1px; +} +@media (min-width: 960px) { +.VPNavBar.has-sidebar .divider[data-v-6aa21345] { + padding-left: var(--vp-sidebar-width); +} +} +@media (min-width: 1440px) { +.VPNavBar.has-sidebar .divider[data-v-6aa21345] { + padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)); +} +} +.divider-line[data-v-6aa21345] { + width: 100%; + height: 1px; + transition: background-color 0.5s; +} +.VPNavBar:not(.home) .divider-line[data-v-6aa21345] { + background-color: var(--vp-c-gutter); +} +@media (min-width: 960px) { +.VPNavBar:not(.home.top) .divider-line[data-v-6aa21345] { + background-color: var(--vp-c-gutter); +} +.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-6aa21345] { + background-color: var(--vp-c-gutter); +} +} + +.VPNavScreenAppearance[data-v-b44890b2] { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 8px; + padding: 12px 14px 12px 16px; + background-color: var(--vp-c-bg-soft); +} +.text[data-v-b44890b2] { + line-height: 24px; + font-size: 12px; + font-weight: 500; + color: var(--vp-c-text-2); +} + +.VPNavScreenMenuLink[data-v-df37e6dd] { + display: block; + border-bottom: 1px solid var(--vp-c-divider); + padding: 12px 0 11px; + line-height: 24px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); + transition: + border-color 0.25s, + color 0.25s; +} +.VPNavScreenMenuLink[data-v-df37e6dd]:hover { + color: var(--vp-c-brand-1); +} + +.VPNavScreenMenuGroupLink[data-v-3e9c20e4] { + display: block; + margin-left: 12px; + line-height: 32px; + font-size: 14px; + font-weight: 400; + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.VPNavScreenMenuGroupLink[data-v-3e9c20e4]:hover { + color: var(--vp-c-brand-1); +} + +.VPNavScreenMenuGroupSection[data-v-8133b170] { + display: block; +} +.title[data-v-8133b170] { + line-height: 32px; + font-size: 13px; + font-weight: 700; + color: var(--vp-c-text-2); + transition: color 0.25s; +} + +.VPNavScreenMenuGroup[data-v-b9ab8c58] { + border-bottom: 1px solid var(--vp-c-divider); + height: 48px; + overflow: hidden; + transition: border-color 0.5s; +} +.VPNavScreenMenuGroup .items[data-v-b9ab8c58] { + visibility: hidden; +} +.VPNavScreenMenuGroup.open .items[data-v-b9ab8c58] { + visibility: visible; +} +.VPNavScreenMenuGroup.open[data-v-b9ab8c58] { + padding-bottom: 10px; + height: auto; +} +.VPNavScreenMenuGroup.open .button[data-v-b9ab8c58] { + padding-bottom: 6px; + color: var(--vp-c-brand-1); +} +.VPNavScreenMenuGroup.open .button-icon[data-v-b9ab8c58] { + /*rtl:ignore*/ + transform: rotate(45deg); +} +.button[data-v-b9ab8c58] { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 4px 11px 0; + width: 100%; + line-height: 24px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); + transition: color 0.25s; +} +.button[data-v-b9ab8c58]:hover { + color: var(--vp-c-brand-1); +} +.button-icon[data-v-b9ab8c58] { + transition: transform 0.25s; +} +.group[data-v-b9ab8c58]:first-child { + padding-top: 0px; +} +.group + .group[data-v-b9ab8c58], +.group + .item[data-v-b9ab8c58] { + padding-top: 4px; +} + +.VPNavScreenTranslations[data-v-858fe1a4] { + height: 24px; + overflow: hidden; +} +.VPNavScreenTranslations.open[data-v-858fe1a4] { + height: auto; +} +.title[data-v-858fe1a4] { + display: flex; + align-items: center; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-text-1); +} +.icon[data-v-858fe1a4] { + font-size: 16px; +} +.icon.lang[data-v-858fe1a4] { + margin-right: 8px; +} +.icon.chevron[data-v-858fe1a4] { + margin-left: 4px; +} +.list[data-v-858fe1a4] { + padding: 4px 0 0 24px; +} +.link[data-v-858fe1a4] { + line-height: 32px; + font-size: 13px; + color: var(--vp-c-text-1); +} + +.VPNavScreen[data-v-f2779853] { + position: fixed; + top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px)); + /*rtl:ignore*/ + right: 0; + bottom: 0; + /*rtl:ignore*/ + left: 0; + padding: 0 32px; + width: 100%; + background-color: var(--vp-nav-screen-bg-color); + overflow-y: auto; + transition: background-color 0.25s; + pointer-events: auto; +} +.VPNavScreen.fade-enter-active[data-v-f2779853], +.VPNavScreen.fade-leave-active[data-v-f2779853] { + transition: opacity 0.25s; +} +.VPNavScreen.fade-enter-active .container[data-v-f2779853], +.VPNavScreen.fade-leave-active .container[data-v-f2779853] { + transition: transform 0.25s ease; +} +.VPNavScreen.fade-enter-from[data-v-f2779853], +.VPNavScreen.fade-leave-to[data-v-f2779853] { + opacity: 0; +} +.VPNavScreen.fade-enter-from .container[data-v-f2779853], +.VPNavScreen.fade-leave-to .container[data-v-f2779853] { + transform: translateY(-8px); +} +@media (min-width: 768px) { +.VPNavScreen[data-v-f2779853] { + display: none; +} +} +.container[data-v-f2779853] { + margin: 0 auto; + padding: 24px 0 96px; + max-width: 288px; +} +.menu + .translations[data-v-f2779853], +.menu + .appearance[data-v-f2779853], +.translations + .appearance[data-v-f2779853] { + margin-top: 24px; +} +.menu + .social-links[data-v-f2779853] { + margin-top: 16px; +} +.appearance + .social-links[data-v-f2779853] { + margin-top: 16px; +} + +.VPNav[data-v-ae24b3ad] { + position: relative; + top: var(--vp-layout-top-height, 0px); + /*rtl:ignore*/ + left: 0; + z-index: var(--vp-z-index-nav); + width: 100%; + pointer-events: none; + transition: background-color 0.5s; +} +@media (min-width: 960px) { +.VPNav[data-v-ae24b3ad] { + position: fixed; +} +} + +.VPSidebarItem.level-0[data-v-b3fd67f8] { + padding-bottom: 24px; +} +.VPSidebarItem.collapsed.level-0[data-v-b3fd67f8] { + padding-bottom: 10px; +} +.item[data-v-b3fd67f8] { + position: relative; + display: flex; + width: 100%; +} +.VPSidebarItem.collapsible > .item[data-v-b3fd67f8] { + cursor: pointer; +} +.indicator[data-v-b3fd67f8] { + position: absolute; + top: 6px; + bottom: 6px; + left: -17px; + width: 2px; + border-radius: 2px; + transition: background-color 0.25s; +} +.VPSidebarItem.level-2.is-active > .item > .indicator[data-v-b3fd67f8], +.VPSidebarItem.level-3.is-active > .item > .indicator[data-v-b3fd67f8], +.VPSidebarItem.level-4.is-active > .item > .indicator[data-v-b3fd67f8], +.VPSidebarItem.level-5.is-active > .item > .indicator[data-v-b3fd67f8] { + background-color: var(--vp-c-brand-1); +} +.link[data-v-b3fd67f8] { + display: flex; + align-items: center; + flex-grow: 1; +} +.text[data-v-b3fd67f8] { + flex-grow: 1; + padding: 4px 0; + line-height: 24px; + font-size: 14px; + transition: color 0.25s; +} +.VPSidebarItem.level-0 .text[data-v-b3fd67f8] { + font-weight: 700; + color: var(--vp-c-text-1); +} +.VPSidebarItem.level-1 .text[data-v-b3fd67f8], +.VPSidebarItem.level-2 .text[data-v-b3fd67f8], +.VPSidebarItem.level-3 .text[data-v-b3fd67f8], +.VPSidebarItem.level-4 .text[data-v-b3fd67f8], +.VPSidebarItem.level-5 .text[data-v-b3fd67f8] { + font-weight: 500; + color: var(--vp-c-text-2); +} +.VPSidebarItem.level-0.is-link > .item > .link:hover .text[data-v-b3fd67f8], +.VPSidebarItem.level-1.is-link > .item > .link:hover .text[data-v-b3fd67f8], +.VPSidebarItem.level-2.is-link > .item > .link:hover .text[data-v-b3fd67f8], +.VPSidebarItem.level-3.is-link > .item > .link:hover .text[data-v-b3fd67f8], +.VPSidebarItem.level-4.is-link > .item > .link:hover .text[data-v-b3fd67f8], +.VPSidebarItem.level-5.is-link > .item > .link:hover .text[data-v-b3fd67f8] { + color: var(--vp-c-brand-1); +} +.VPSidebarItem.level-0.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-1.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-2.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-3.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-4.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-5.has-active > .item > .text[data-v-b3fd67f8], +.VPSidebarItem.level-0.has-active > .item > .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-1.has-active > .item > .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-2.has-active > .item > .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-3.has-active > .item > .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-4.has-active > .item > .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-5.has-active > .item > .link > .text[data-v-b3fd67f8] { + color: var(--vp-c-text-1); +} +.VPSidebarItem.level-0.is-active > .item .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-1.is-active > .item .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-2.is-active > .item .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-3.is-active > .item .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-4.is-active > .item .link > .text[data-v-b3fd67f8], +.VPSidebarItem.level-5.is-active > .item .link > .text[data-v-b3fd67f8] { + color: var(--vp-c-brand-1); +} +.caret[data-v-b3fd67f8] { + display: flex; + justify-content: center; + align-items: center; + margin-right: -7px; + width: 32px; + height: 32px; + color: var(--vp-c-text-3); + cursor: pointer; + transition: color 0.25s; + flex-shrink: 0; +} +.item:hover .caret[data-v-b3fd67f8] { + color: var(--vp-c-text-2); +} +.item:hover .caret[data-v-b3fd67f8]:hover { + color: var(--vp-c-text-1); +} +.caret-icon[data-v-b3fd67f8] { + font-size: 18px; + /*rtl:ignore*/ + transform: rotate(90deg); + transition: transform 0.25s; +} +.VPSidebarItem.collapsed .caret-icon[data-v-b3fd67f8] { + transform: rotate(0)/*rtl:rotate(180deg)*/; +} +.VPSidebarItem.level-1 .items[data-v-b3fd67f8], +.VPSidebarItem.level-2 .items[data-v-b3fd67f8], +.VPSidebarItem.level-3 .items[data-v-b3fd67f8], +.VPSidebarItem.level-4 .items[data-v-b3fd67f8], +.VPSidebarItem.level-5 .items[data-v-b3fd67f8] { + border-left: 1px solid var(--vp-c-divider); + padding-left: 16px; +} +.VPSidebarItem.collapsed .items[data-v-b3fd67f8] { + display: none; +} + +.no-transition[data-v-c40bc020] .caret-icon { + transition: none; +} +.group + .group[data-v-c40bc020] { + border-top: 1px solid var(--vp-c-divider); + padding-top: 10px; +} +@media (min-width: 960px) { +.group[data-v-c40bc020] { + padding-top: 10px; + width: calc(var(--vp-sidebar-width) - 64px); +} +} + +.VPSidebar[data-v-319d5ca6] { + position: fixed; + top: var(--vp-layout-top-height, 0px); + bottom: 0; + left: 0; + z-index: var(--vp-z-index-sidebar); + padding: 32px 32px 96px; + width: calc(100vw - 64px); + max-width: 320px; + background-color: var(--vp-sidebar-bg-color); + opacity: 0; + box-shadow: var(--vp-c-shadow-3); + overflow-x: hidden; + overflow-y: auto; + transform: translateX(-100%); + transition: opacity 0.5s, transform 0.25s ease; + overscroll-behavior: contain; +} +.VPSidebar.open[data-v-319d5ca6] { + opacity: 1; + visibility: visible; + transform: translateX(0); + transition: opacity 0.25s, + transform 0.5s cubic-bezier(0.19, 1, 0.22, 1); +} +.dark .VPSidebar[data-v-319d5ca6] { + box-shadow: var(--vp-shadow-1); +} +@media (min-width: 960px) { +.VPSidebar[data-v-319d5ca6] { + padding-top: var(--vp-nav-height); + width: var(--vp-sidebar-width); + max-width: 100%; + background-color: var(--vp-sidebar-bg-color); + opacity: 1; + visibility: visible; + box-shadow: none; + transform: translateX(0); +} +} +@media (min-width: 1440px) { +.VPSidebar[data-v-319d5ca6] { + padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2)); + width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px); +} +} +@media (min-width: 960px) { +.curtain[data-v-319d5ca6] { + position: sticky; + top: -64px; + left: 0; + z-index: 1; + margin-top: calc(var(--vp-nav-height) * -1); + margin-right: -32px; + margin-left: -32px; + height: var(--vp-nav-height); + background-color: var(--vp-sidebar-bg-color); +} +} +.nav[data-v-319d5ca6] { + outline: 0; +} + +.VPSkipLink[data-v-0b0ada53] { + top: 8px; + left: 8px; + padding: 8px 16px; + z-index: 999; + border-radius: 8px; + font-size: 12px; + font-weight: bold; + text-decoration: none; + color: var(--vp-c-brand-1); + box-shadow: var(--vp-shadow-3); + background-color: var(--vp-c-bg); +} +.VPSkipLink[data-v-0b0ada53]:focus { + height: auto; + width: auto; + clip: auto; + clip-path: none; +} +@media (min-width: 1280px) { +.VPSkipLink[data-v-0b0ada53] { + top: 14px; + left: 16px; +} +} + +.Layout[data-v-5d98c3a5] { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.VPHomeSponsors[data-v-3d121b4a] { + border-top: 1px solid var(--vp-c-gutter); + padding-top: 88px !important; +} +.VPHomeSponsors[data-v-3d121b4a] { + margin: 96px 0; +} +@media (min-width: 768px) { +.VPHomeSponsors[data-v-3d121b4a] { + margin: 128px 0; +} +} +.VPHomeSponsors[data-v-3d121b4a] { + padding: 0 24px; +} +@media (min-width: 768px) { +.VPHomeSponsors[data-v-3d121b4a] { + padding: 0 48px; +} +} +@media (min-width: 960px) { +.VPHomeSponsors[data-v-3d121b4a] { + padding: 0 64px; +} +} +.container[data-v-3d121b4a] { + margin: 0 auto; + max-width: 1152px; +} +.love[data-v-3d121b4a] { + margin: 0 auto; + width: fit-content; + font-size: 28px; + color: var(--vp-c-text-3); +} +.icon[data-v-3d121b4a] { + display: inline-block; +} +.message[data-v-3d121b4a] { + margin: 0 auto; + padding-top: 10px; + max-width: 320px; + text-align: center; + line-height: 24px; + font-size: 16px; + font-weight: 500; + color: var(--vp-c-text-2); +} +.sponsors[data-v-3d121b4a] { + padding-top: 32px; +} +.action[data-v-3d121b4a] { + padding-top: 40px; + text-align: center; +} + +.VPTeamMembersItem[data-v-f3fa364a] { + display: flex; + flex-direction: column; + gap: 2px; + border-radius: 12px; + width: 100%; + height: 100%; + overflow: hidden; +} +.VPTeamMembersItem.small .profile[data-v-f3fa364a] { + padding: 32px; +} +.VPTeamMembersItem.small .data[data-v-f3fa364a] { + padding-top: 20px; +} +.VPTeamMembersItem.small .avatar[data-v-f3fa364a] { + width: 64px; + height: 64px; +} +.VPTeamMembersItem.small .name[data-v-f3fa364a] { + line-height: 24px; + font-size: 16px; +} +.VPTeamMembersItem.small .affiliation[data-v-f3fa364a] { + padding-top: 4px; + line-height: 20px; + font-size: 14px; +} +.VPTeamMembersItem.small .desc[data-v-f3fa364a] { + padding-top: 12px; + line-height: 20px; + font-size: 14px; +} +.VPTeamMembersItem.small .links[data-v-f3fa364a] { + margin: 0 -16px -20px; + padding: 10px 0 0; +} +.VPTeamMembersItem.medium .profile[data-v-f3fa364a] { + padding: 48px 32px; +} +.VPTeamMembersItem.medium .data[data-v-f3fa364a] { + padding-top: 24px; + text-align: center; +} +.VPTeamMembersItem.medium .avatar[data-v-f3fa364a] { + width: 96px; + height: 96px; +} +.VPTeamMembersItem.medium .name[data-v-f3fa364a] { + letter-spacing: 0.15px; + line-height: 28px; + font-size: 20px; +} +.VPTeamMembersItem.medium .affiliation[data-v-f3fa364a] { + padding-top: 4px; + font-size: 16px; +} +.VPTeamMembersItem.medium .desc[data-v-f3fa364a] { + padding-top: 16px; + max-width: 288px; + font-size: 16px; +} +.VPTeamMembersItem.medium .links[data-v-f3fa364a] { + margin: 0 -16px -12px; + padding: 16px 12px 0; +} +.profile[data-v-f3fa364a] { + flex-grow: 1; + background-color: var(--vp-c-bg-soft); +} +.data[data-v-f3fa364a] { + text-align: center; +} +.avatar[data-v-f3fa364a] { + position: relative; + flex-shrink: 0; + margin: 0 auto; + border-radius: 50%; + box-shadow: var(--vp-shadow-3); +} +.avatar-img[data-v-f3fa364a] { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: 50%; + object-fit: cover; +} +.name[data-v-f3fa364a] { + margin: 0; + font-weight: 600; +} +.affiliation[data-v-f3fa364a] { + margin: 0; + font-weight: 500; + color: var(--vp-c-text-2); +} +.org.link[data-v-f3fa364a] { + color: var(--vp-c-text-2); + transition: color 0.25s; +} +.org.link[data-v-f3fa364a]:hover { + color: var(--vp-c-brand-1); +} +.desc[data-v-f3fa364a] { + margin: 0 auto; +} +.desc[data-v-f3fa364a] a { + font-weight: 500; + color: var(--vp-c-brand-1); + text-decoration-style: dotted; + transition: color 0.25s; +} +.links[data-v-f3fa364a] { + display: flex; + justify-content: center; + height: 56px; +} +.sp-link[data-v-f3fa364a] { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + padding: 16px; + font-size: 14px; + font-weight: 500; + color: var(--vp-c-sponsor); + background-color: var(--vp-c-bg-soft); + transition: color 0.25s, background-color 0.25s; +} +.sp .sp-link.link[data-v-f3fa364a]:hover, +.sp .sp-link.link[data-v-f3fa364a]:focus { + outline: none; + color: var(--vp-c-white); + background-color: var(--vp-c-sponsor); +} +.sp-icon[data-v-f3fa364a] { + margin-right: 8px; + font-size: 16px; +} + +.VPTeamMembers.small .container[data-v-6cb0dbc4] { + grid-template-columns: repeat(auto-fit, minmax(224px, 1fr)); +} +.VPTeamMembers.small.count-1 .container[data-v-6cb0dbc4] { + max-width: 276px; +} +.VPTeamMembers.small.count-2 .container[data-v-6cb0dbc4] { + max-width: calc(276px * 2 + 24px); +} +.VPTeamMembers.small.count-3 .container[data-v-6cb0dbc4] { + max-width: calc(276px * 3 + 24px * 2); +} +.VPTeamMembers.medium .container[data-v-6cb0dbc4] { + grid-template-columns: repeat(auto-fit, minmax(256px, 1fr)); +} +@media (min-width: 375px) { +.VPTeamMembers.medium .container[data-v-6cb0dbc4] { + grid-template-columns: repeat(auto-fit, minmax(288px, 1fr)); +} +} +.VPTeamMembers.medium.count-1 .container[data-v-6cb0dbc4] { + max-width: 368px; +} +.VPTeamMembers.medium.count-2 .container[data-v-6cb0dbc4] { + max-width: calc(368px * 2 + 24px); +} +.container[data-v-6cb0dbc4] { + display: grid; + gap: 24px; + margin: 0 auto; + max-width: 1152px; +} + +.VPTeamPage[data-v-7c57f839] { + margin: 96px 0; +} +@media (min-width: 768px) { +.VPTeamPage[data-v-7c57f839] { + margin: 128px 0; +} +} +.VPHome .VPTeamPageTitle[data-v-7c57f839-s] { + border-top: 1px solid var(--vp-c-gutter); + padding-top: 88px !important; +} +.VPTeamPageSection + .VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers + .VPTeamPageSection[data-v-7c57f839-s] { + margin-top: 64px; +} +.VPTeamMembers + .VPTeamMembers[data-v-7c57f839-s] { + margin-top: 24px; +} +@media (min-width: 768px) { +.VPTeamPageTitle + .VPTeamPageSection[data-v-7c57f839-s] { + margin-top: 16px; +} +.VPTeamPageSection + .VPTeamPageSection[data-v-7c57f839-s],.VPTeamMembers + .VPTeamPageSection[data-v-7c57f839-s] { + margin-top: 96px; +} +} +.VPTeamMembers[data-v-7c57f839-s] { + padding: 0 24px; +} +@media (min-width: 768px) { +.VPTeamMembers[data-v-7c57f839-s] { + padding: 0 48px; +} +} +@media (min-width: 960px) { +.VPTeamMembers[data-v-7c57f839-s] { + padding: 0 64px; +} +} + +.VPTeamPageSection[data-v-b1a88750] { + padding: 0 32px; +} +@media (min-width: 768px) { +.VPTeamPageSection[data-v-b1a88750] { + padding: 0 48px; +} +} +@media (min-width: 960px) { +.VPTeamPageSection[data-v-b1a88750] { + padding: 0 64px; +} +} +.title[data-v-b1a88750] { + position: relative; + margin: 0 auto; + max-width: 1152px; + text-align: center; + color: var(--vp-c-text-2); +} +.title-line[data-v-b1a88750] { + position: absolute; + top: 16px; + left: 0; + width: 100%; + height: 1px; + background-color: var(--vp-c-divider); +} +.title-text[data-v-b1a88750] { + position: relative; + display: inline-block; + padding: 0 24px; + letter-spacing: 0; + line-height: 32px; + font-size: 20px; + font-weight: 500; + background-color: var(--vp-c-bg); +} +.lead[data-v-b1a88750] { + margin: 0 auto; + max-width: 480px; + padding-top: 12px; + text-align: center; + line-height: 24px; + font-size: 16px; + font-weight: 500; + color: var(--vp-c-text-2); +} +.members[data-v-b1a88750] { + padding-top: 40px; +} + +.VPTeamPageTitle[data-v-bf2cbdac] { + padding: 48px 32px; + text-align: center; +} +@media (min-width: 768px) { +.VPTeamPageTitle[data-v-bf2cbdac] { + padding: 64px 48px 48px; +} +} +@media (min-width: 960px) { +.VPTeamPageTitle[data-v-bf2cbdac] { + padding: 80px 64px 48px; +} +} +.title[data-v-bf2cbdac] { + letter-spacing: 0; + line-height: 44px; + font-size: 36px; + font-weight: 500; +} +@media (min-width: 768px) { +.title[data-v-bf2cbdac] { + letter-spacing: -0.5px; + line-height: 56px; + font-size: 48px; +} +} +.lead[data-v-bf2cbdac] { + margin: 0 auto; + max-width: 512px; + padding-top: 12px; + line-height: 24px; + font-size: 16px; + font-weight: 500; + color: var(--vp-c-text-2); +} +@media (min-width: 768px) { +.lead[data-v-bf2cbdac] { + max-width: 592px; + letter-spacing: 0.15px; + line-height: 28px; + font-size: 20px; +} +} + +.VPLocalSearchBox[data-v-ce626c7c] { + position: fixed; + z-index: 100; + inset: 0; + display: flex; +} +.backdrop[data-v-ce626c7c] { + position: absolute; + inset: 0; + background: var(--vp-backdrop-bg-color); + transition: opacity 0.5s; +} +.shell[data-v-ce626c7c] { + position: relative; + padding: 12px; + margin: 64px auto; + display: flex; + flex-direction: column; + gap: 16px; + background: var(--vp-local-search-bg); + width: min(100vw - 60px, 900px); + height: min-content; + max-height: min(100vh - 128px, 900px); + border-radius: 6px; +} +@media (max-width: 767px) { +.shell[data-v-ce626c7c] { + margin: 0; + width: 100vw; + height: 100vh; + max-height: none; + border-radius: 0; +} +} +.search-bar[data-v-ce626c7c] { + border: 1px solid var(--vp-c-divider); + border-radius: 4px; + display: flex; + align-items: center; + padding: 0 12px; + cursor: text; +} +@media (max-width: 767px) { +.search-bar[data-v-ce626c7c] { + padding: 0 8px; +} +} +.search-bar[data-v-ce626c7c]:focus-within { + border-color: var(--vp-c-brand-1); +} +.local-search-icon[data-v-ce626c7c] { + display: block; + font-size: 18px; +} +.navigate-icon[data-v-ce626c7c] { + display: block; + font-size: 14px; +} +.search-icon[data-v-ce626c7c] { + margin: 8px; +} +@media (max-width: 767px) { +.search-icon[data-v-ce626c7c] { + display: none; +} +} +.search-input[data-v-ce626c7c] { + padding: 6px 12px; + font-size: inherit; + width: 100%; +} +@media (max-width: 767px) { +.search-input[data-v-ce626c7c] { + padding: 6px 4px; +} +} +.search-actions[data-v-ce626c7c] { + display: flex; + gap: 4px; +} +@media (any-pointer: coarse) { +.search-actions[data-v-ce626c7c] { + gap: 8px; +} +} +@media (min-width: 769px) { +.search-actions.before[data-v-ce626c7c] { + display: none; +} +} +.search-actions button[data-v-ce626c7c] { + padding: 8px; +} +.search-actions button[data-v-ce626c7c]:not([disabled]):hover, +.toggle-layout-button.detailed-list[data-v-ce626c7c] { + color: var(--vp-c-brand-1); +} +.search-actions button.clear-button[data-v-ce626c7c]:disabled { + opacity: 0.37; +} +.search-keyboard-shortcuts[data-v-ce626c7c] { + font-size: 0.8rem; + opacity: 75%; + display: flex; + flex-wrap: wrap; + gap: 16px; + line-height: 14px; +} +.search-keyboard-shortcuts span[data-v-ce626c7c] { + display: flex; + align-items: center; + gap: 4px; +} +@media (max-width: 767px) { +.search-keyboard-shortcuts[data-v-ce626c7c] { + display: none; +} +} +.search-keyboard-shortcuts kbd[data-v-ce626c7c] { + background: rgba(128, 128, 128, 0.1); + border-radius: 4px; + padding: 3px 6px; + min-width: 24px; + display: inline-block; + text-align: center; + vertical-align: middle; + border: 1px solid rgba(128, 128, 128, 0.15); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); +} +.results[data-v-ce626c7c] { + display: flex; + flex-direction: column; + gap: 6px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; +} +.result[data-v-ce626c7c] { + display: flex; + align-items: center; + gap: 8px; + border-radius: 4px; + transition: none; + line-height: 1rem; + border: solid 2px var(--vp-local-search-result-border); + outline: none; +} +.result > div[data-v-ce626c7c] { + margin: 12px; + width: 100%; + overflow: hidden; +} +@media (max-width: 767px) { +.result > div[data-v-ce626c7c] { + margin: 8px; +} +} +.titles[data-v-ce626c7c] { + display: flex; + flex-wrap: wrap; + gap: 4px; + position: relative; + z-index: 1001; + padding: 2px 0; +} +.title[data-v-ce626c7c] { + display: flex; + align-items: center; + gap: 4px; +} +.title.main[data-v-ce626c7c] { + font-weight: 500; +} +.title-icon[data-v-ce626c7c] { + opacity: 0.5; + font-weight: 500; + color: var(--vp-c-brand-1); +} +.title svg[data-v-ce626c7c] { + opacity: 0.5; +} +.result.selected[data-v-ce626c7c] { + --vp-local-search-result-bg: var(--vp-local-search-result-selected-bg); + border-color: var(--vp-local-search-result-selected-border); +} +.excerpt-wrapper[data-v-ce626c7c] { + position: relative; +} +.excerpt[data-v-ce626c7c] { + opacity: 50%; + pointer-events: none; + max-height: 140px; + overflow: hidden; + position: relative; + margin-top: 4px; +} +.result.selected .excerpt[data-v-ce626c7c] { + opacity: 1; +} +.excerpt[data-v-ce626c7c] * { + font-size: 0.8rem !important; + line-height: 130% !important; +} +.titles[data-v-ce626c7c] mark, +.excerpt[data-v-ce626c7c] mark { + background-color: var(--vp-local-search-highlight-bg); + color: var(--vp-local-search-highlight-text); + border-radius: 2px; + padding: 0 2px; +} +.excerpt[data-v-ce626c7c] .vp-code-group .tabs { + display: none; +} +.excerpt[data-v-ce626c7c] .vp-code-group div[class*='language-'] { + border-radius: 8px !important; +} +.excerpt-gradient-bottom[data-v-ce626c7c] { + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 8px; + background: linear-gradient(transparent, var(--vp-local-search-result-bg)); + z-index: 1000; +} +.excerpt-gradient-top[data-v-ce626c7c] { + position: absolute; + top: -1px; + left: 0; + width: 100%; + height: 8px; + background: linear-gradient(var(--vp-local-search-result-bg), transparent); + z-index: 1000; +} +.result.selected .titles[data-v-ce626c7c], +.result.selected .title-icon[data-v-ce626c7c] { + color: var(--vp-c-brand-1) !important; +} +.no-results[data-v-ce626c7c] { + font-size: 0.9rem; + text-align: center; + padding: 12px; +} +svg[data-v-ce626c7c] { + flex: none; +} diff --git a/docs/.vitepress/.temp/index.md.js b/docs/.vitepress/.temp/index.md.js new file mode 100644 index 0000000..bba97dc --- /dev/null +++ b/docs/.vitepress/.temp/index.md.js @@ -0,0 +1,32 @@ +import { ssrRenderAttrs, ssrRenderStyle } from "vue/server-renderer"; +import { useSSRContext } from "vue"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const __pageData = JSON.parse(`{"title":"ai-coding-kit","description":"","frontmatter":{"layout":"home","title":"ai-coding-kit","hero":{"name":"ai-coding-kit","text":"One Kit. All AI Coding Tools.","tagline":"Agent Skills management, MCP configuration sync, iOS engineering rules, and Universal RAG Gateway — unified for 8+ AI coding platforms.","image":false,"actions":[{"theme":"brand","text":"Get Started","link":"/ios-engineer/"},{"theme":"alt","text":"View on GitHub","link":"https://github.com/i-stack/ai-coding-kit"}]},"features":[{"icon":"🧠","title":"Agent Skills Engineering","details":"Define skills once, sync to Claude Code, Codex CLI, Cursor, Gemini CLI, CodeBuddy, Continue, Cline, and Xcode Coding Assistant — with structured evolution governance."},{"icon":"⚙️","title":"MCP Config Sync","details":"Single source of truth for MCP servers, API keys, and model settings. Auto-render to each platform's native config format."},{"icon":"🍎","title":"iOS Engineering Rules","details":"Production-grade Swift / SwiftUI / UIKit rules with 40+ rule IDs, symptom routing, task triage, and auto-evolution — maintained by an Agent Skill system."},{"icon":"🌐","title":"Universal RAG Gateway","details":"TypeScript / Fastify RAG gateway with OpenAI-compatible API — local memory, semantic retrieval, and multi-provider routing."},{"icon":"🔒","title":"Global Engineering Discipline","details":"Six global skills spanning security compliance, epistemic integrity, logical reasoning, cognitive expansion, and problem analysis — apply to any platform."},{"icon":"🚀","title":"Quick Start","details":"One clone, one secrets file, one sync command. Supports Homebrew and npm installation."}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1783251060000}`); +const _sfc_main = { name: "index.md" }; +function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { + _push(`

    Quick Start

    bash
    # Clone & configure
    +git clone https://github.com/i-stack/ai-coding-kit.git
    +cd ai-coding-kit
    +
    +# Edit your secrets (the only file you need to touch)
    +cp env/secrets.json.example env/secrets.json
    +$EDITOR env/secrets.json
    +
    +# One command to sync everything
    +bash sync.sh

    Or install via package manager

    bash
    # Homebrew
    +brew install i-stack/tap/ai-coding-kit
    +
    +# npm
    +npm install -g @i-stack/ai-coding-kit

    Platform Support

    ToolWhat Gets Synced
    Cursor.cursor/mcp.json
    CodeBuddy.codebuddy/mcp.json, models.json, skills/
    Claude Code.claude.json, settings.json, skills/
    Codex CLI.codex/config.toml, mcp.generated.toml
    Gemini CLIEnvironment variables
    Continue.continue/config.yaml
    Cline (VSCode)MCP settings JSON, skills/
    Xcode Coding AssistantCodex + Claude Agent config paths

    Modules

    ModuleDescription
    skills-engineering/Agent Skill content, multi-platform sync, governed evolution
    sync/MCP config sync engine — injects secrets, renders to native formats
    env/Config data source (secrets + MCP definitions + platform configs)
    rag-gateway/TypeScript / Fastify Universal RAG Gateway (OpenAI-compatible API)
    hooks/Project hooks (xmcp init, etc.)
    .githooks/Git commit/push guards (pre-commit + pre-push)
    `); +} +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("index.md"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const index = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]); +export { + __pageData, + index as default +}; diff --git a/docs/.vitepress/.temp/ios-engineer_index.md.js b/docs/.vitepress/.temp/ios-engineer_index.md.js new file mode 100644 index 0000000..8423e6d --- /dev/null +++ b/docs/.vitepress/.temp/ios-engineer_index.md.js @@ -0,0 +1,38 @@ +import { resolveComponent, useSSRContext } from "vue"; +import { ssrRenderAttrs, ssrRenderComponent } from "vue/server-renderer"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const __pageData = JSON.parse('{"title":"iOS Engineer","description":"","frontmatter":{},"headers":[],"relativePath":"ios-engineer/index.md","filePath":"ios-engineer/index.md","lastUpdated":1783251060000}'); +const _sfc_main = { name: "ios-engineer/index.md" }; +function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { + const _component_Badge = resolveComponent("Badge"); + _push(`

    iOS Engineer

    `); + _push(ssrRenderComponent(_component_Badge, { + type: "tip", + text: "v3.0.0" + }, null, _parent)); + _push(`

    iOS / Swift / SwiftUI / UIKit / Xcode / CocoaPods / SPM engineering — architecture, concurrency, networking, performance, crash debugging, code review, refactoring, migration, testing.

    This is the primary Agent Skill in ai-coding-kit, providing production-grade AI coding rules for iOS development.

    Supported Locales

    English (en-US) · 简体中文 (zh-CN). The skill auto-matches your language.

    Architecture

    The skill is organized as a layered system:

    ios-engineer/
    +├── SKILL.md              # Entry point: routing, triggers, output templates
    +├── references/           # 34 domain reference files (zh-CN)
    +│   ├── rule_index.md     # Canonical Rule ID registry
    +│   ├── self_evolution.md # Auto-evolution governance
    +│   └── ...               # 31 domain-specific references
    +├── i18n/en-US/           # English governance-layer mirrors
    +│   └── references/
    +├── scripts/              # 27 validation & evolution scripts
    +├── evolution/            # Proposal-driven evolution pipeline
    +│   ├── proposals/        # Active/in-review proposals
    +│   ├── archive/          # Archived/implemented proposals
    +│   └── hooks/            # Evolution guard scripts
    +└── snapshots/            # Evolution snapshots for consistency checks

    Rule System

    The skill enforces 40+ rule IDs across 5 categories:

    CategoryPrefixCountScope
    Iron RulesIR-NNN3Always enforced
    Global RulesGR-NNN9Cross-platform (epistemic, logic, discipline)
    Symptom RoutingSYM-NNN7Auto-route symptoms → references
    Task RoutingROUTE-NNN10Auto-route task types → references
    Output TemplatesOUT-NNN6Structured output formats

    See the Rule Index for the complete registry.

    Key Rules

    IR-001 — Language Anchoring

    Output language matches the user's input language. No forced Chinese output.

    IR-006 — Version Context Block

    All concurrency / availability / SwiftUI behavior / network cancellation answers require a version context block before conclusions.

    IR-011 — Cognitive Adversary Mode

    When triggered: output restatement, strongest counter-argument, hidden assumptions, failure conditions, falsifiable conditions, position flip, conformity self-check, confidence level, conclusion.

    Evolution Governance

    The skill evolves through a proposal-driven pipeline:

    1. Propose — Create a proposal in evolution/proposals/
    2. Validate — Run scripts/validate_skill_evolution.sh (14-step check)
    3. Implement — Add/modify references; update rule_index.md
    4. Promote — Archive proposal; snapshot the skill state

    All changes to SKILL.md or references/ are gated by the pre-commit hook, which requires a staged evolution proposal in the same commit.

    `); +} +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("ios-engineer/index.md"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const index = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]); +export { + __pageData, + index as default +}; diff --git a/docs/.vitepress/.temp/ios-engineer_references.md.js b/docs/.vitepress/.temp/ios-engineer_references.md.js new file mode 100644 index 0000000..807fb75 --- /dev/null +++ b/docs/.vitepress/.temp/ios-engineer_references.md.js @@ -0,0 +1,19 @@ +import { ssrRenderAttrs } from "vue/server-renderer"; +import { useSSRContext } from "vue"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const __pageData = JSON.parse('{"title":"References","description":"","frontmatter":{},"headers":[],"relativePath":"ios-engineer/references.md","filePath":"ios-engineer/references.md","lastUpdated":1783251060000}'); +const _sfc_main = { name: "ios-engineer/references.md" }; +function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { + _push(`

    References

    The iOS Engineer skill includes 34 domain reference files covering the full iOS / Swift engineering lifecycle.

    How references are used

    References are loaded by the AI agent at runtime based on symptom routing (SYM-) or task routing (ROUTE-) rules. They provide detailed domain knowledge for specific scenarios.

    Governance Layer

    ReferenceDescription
    rule_index.mdCanonical Rule ID registry (49 IDs)
    self_evolution.mdAuto-evolution governance rules
    cognitive_adversary_mode.mdCognitive adversary mode specification
    usage_ledger.mdUsage tracking ledger

    Domain References

    ReferenceDomain
    architecture_analysis.mdArchitecture analysis
    architecture_and_network.mdArchitecture & networking
    anti_patterns.mdAnti-patterns
    app_extensions.mdApp extensions
    build_release_and_ci.mdBuild, release & CI
    code_templates.mdCode templates
    decision_records.mdDecision records
    domain_modeling.mdDomain modeling
    examples.mdExamples

    See the full reference directory on GitHub for all 34 files.

    Validation Scripts

    The skill ships with 27 validation and evolution scripts in scripts/:

    ScriptPurpose
    validate_rule_ids.shEnsures rule IDs are consistent between rule_index.md and SKILL.md
    validate_scenario_specs.shValidates scenario specification files
    audit_ref_freshness.shAudits last-verified dates in reference files
    validate_skill_evolution.sh14-step comprehensive evolution validation
    check_snapshot_consistency.shCompares current skill state against snapshots
    validate_usage_ledger.shValidates usage ledger integrity
    `); +} +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("ios-engineer/references.md"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const references = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]); +export { + __pageData, + references as default +}; diff --git a/docs/.vitepress/.temp/ios-engineer_rule-index.md.js b/docs/.vitepress/.temp/ios-engineer_rule-index.md.js new file mode 100644 index 0000000..8846528 --- /dev/null +++ b/docs/.vitepress/.temp/ios-engineer_rule-index.md.js @@ -0,0 +1,25 @@ +import { resolveComponent, useSSRContext } from "vue"; +import { ssrRenderAttrs, ssrRenderComponent } from "vue/server-renderer"; +import { _ as _export_sfc } from "./plugin-vue_export-helper.1tPrXgE0.js"; +const __pageData = JSON.parse('{"title":"Rule Index","description":"","frontmatter":{},"headers":[],"relativePath":"ios-engineer/rule-index.md","filePath":"ios-engineer/rule-index.md","lastUpdated":1783251060000}'); +const _sfc_main = { name: "ios-engineer/rule-index.md" }; +function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) { + const _component_Badge = resolveComponent("Badge"); + _push(`

    Rule Index

    `); + _push(ssrRenderComponent(_component_Badge, { + type: "tip", + text: "49 IDs registered" + }, null, _parent)); + _push(`

    The canonical Rule ID registry for iOS Engineer. Every rule ID is defined here first, then referenced in SKILL.md. An automated validation script (validate_rule_ids.sh) ensures bidirectional consistency.

    Iron Rules (IR-NNN)

    IDStatusSummary
    IR-001activeOutput language anchors to user's input language
    IR-006activeVersion context block before conclusions on concurrency/availability/SwiftUI/network
    IR-011activeCognitive adversary mode: restatement, counter-argument, hidden assumptions, falsifiability

    Global Rules (GR-NNN)

    Carried by independent global skills, cross-platform. The ios-engineer skill mirrors them for reference.

    IDStatusSummary
    GR-001activeSecurity compliance — never expose credentials
    GR-002activePre-confirmation block when info is insufficient
    GR-003activeSingle root cause (1 primary + max 1 secondary)
    GR-004activeFour-section output (cause → why → fix → verify)
    GR-005activeMinimal fix first
    GR-006activeTool budget gate — 3 failures or 15 turns blocks
    GR-007activeNo code formatting (prevents diff noise)
    GR-008activeChange coverage declaration
    GR-010activeTraceable logic chain with strength indicators

    Symptom Routing (SYM-NNN)

    IDStatusSummary
    SYM-001activeCrash / assertion / force unwrap → root_cause_enforcement
    SYM-002activeUI misalignment / constraint conflicts / list jitter
    SYM-003activeState chaos / async write-back / stale request override
    SYM-004activeRequest failure / auth refresh / pagination
    SYM-005activeLag / slow launch / memory / energy
    SYM-006activeNaming chaos / force unwrap / access control
    SYM-007activeLegacy project chaos / fear of touching modules

    Task Routing (ROUTE-NNN)

    10 routing entries covering: debugging / architecture design / code review / migration / testing / dependency / build & CI / security & permission / data persistence / Core Skills (markdown/code generation).

    Output Templates (OUT-NNN)

    6 output templates for: root cause analysis, architecture review, code review, migration plan, test design, and decision record.


    See the canonical rule_index.md for the complete registry with status and anchor points.

    `); +} +const _sfc_setup = _sfc_main.setup; +_sfc_main.setup = (props, ctx) => { + const ssrContext = useSSRContext(); + (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("ios-engineer/rule-index.md"); + return _sfc_setup ? _sfc_setup(props, ctx) : void 0; +}; +const ruleIndex = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]); +export { + __pageData, + ruleIndex as default +}; diff --git a/docs/.vitepress/.temp/package.json b/docs/.vitepress/.temp/package.json new file mode 100644 index 0000000..25d9dcf --- /dev/null +++ b/docs/.vitepress/.temp/package.json @@ -0,0 +1 @@ +{ "private": true, "type": "module" } \ No newline at end of file diff --git a/docs/.vitepress/.temp/plugin-vue_export-helper.1tPrXgE0.js b/docs/.vitepress/.temp/plugin-vue_export-helper.1tPrXgE0.js new file mode 100644 index 0000000..84d1cb5 --- /dev/null +++ b/docs/.vitepress/.temp/plugin-vue_export-helper.1tPrXgE0.js @@ -0,0 +1,10 @@ +const _export_sfc = (sfc, props) => { + const target = sfc.__vccOpts || sfc; + for (const [key, val] of props) { + target[key] = val; + } + return target; +}; +export { + _export_sfc as _ +}; diff --git a/docs/.vitepress/dist/404.html b/docs/.vitepress/dist/404.html new file mode 100644 index 0000000..9b65912 --- /dev/null +++ b/docs/.vitepress/dist/404.html @@ -0,0 +1,24 @@ + + + + + + 404 | ai-coding-kit + + + + + + + + + + + + + +
    + + + + \ No newline at end of file diff --git a/docs/.vitepress/dist/assets/app.DVfBX2Do.js b/docs/.vitepress/dist/assets/app.DVfBX2Do.js new file mode 100644 index 0000000..44f7d36 --- /dev/null +++ b/docs/.vitepress/dist/assets/app.DVfBX2Do.js @@ -0,0 +1,106 @@ +import { t as theme } from "./chunks/theme.BrkFYulG.js"; +import { R as inBrowser, a3 as useUpdateHead, a4 as RouterSymbol, a5 as initData, a6 as dataSymbol, a7 as Content, a8 as ClientOnly, a9 as siteDataRef, aa as createRouter, ab as pathToFile, ac as createSSRApp, d as defineComponent, u as useData, v as onMounted, s as watchEffect, ad as usePrefetch, ae as useCopyCode, af as useCodeGroups, ag as h } from "./chunks/framework.BcMzFyCJ.js"; +function resolveThemeExtends(theme2) { + if (theme2.extends) { + const base = resolveThemeExtends(theme2.extends); + return { + ...base, + ...theme2, + async enhanceApp(ctx) { + if (base.enhanceApp) + await base.enhanceApp(ctx); + if (theme2.enhanceApp) + await theme2.enhanceApp(ctx); + } + }; + } + return theme2; +} +const Theme = resolveThemeExtends(theme); +const VitePressApp = defineComponent({ + name: "VitePressApp", + setup() { + const { site, lang, dir } = useData(); + onMounted(() => { + watchEffect(() => { + document.documentElement.lang = lang.value; + document.documentElement.dir = dir.value; + }); + }); + if (site.value.router.prefetchLinks) { + usePrefetch(); + } + useCopyCode(); + useCodeGroups(); + if (Theme.setup) + Theme.setup(); + return () => h(Theme.Layout); + } +}); +async function createApp() { + globalThis.__VITEPRESS__ = true; + const router = newRouter(); + const app = newApp(); + app.provide(RouterSymbol, router); + const data = initData(router.route); + app.provide(dataSymbol, data); + app.component("Content", Content); + app.component("ClientOnly", ClientOnly); + Object.defineProperties(app.config.globalProperties, { + $frontmatter: { + get() { + return data.frontmatter.value; + } + }, + $params: { + get() { + return data.page.value.params; + } + } + }); + if (Theme.enhanceApp) { + await Theme.enhanceApp({ + app, + router, + siteData: siteDataRef + }); + } + return { app, router, data }; +} +function newApp() { + return createSSRApp(VitePressApp); +} +function newRouter() { + let isInitialPageLoad = inBrowser; + return createRouter((path) => { + let pageFilePath = pathToFile(path); + let pageModule = null; + if (pageFilePath) { + if (isInitialPageLoad) { + pageFilePath = pageFilePath.replace(/\.js$/, ".lean.js"); + } + if (false) ; + else { + pageModule = import( + /*@vite-ignore*/ + pageFilePath + ); + } + } + if (inBrowser) { + isInitialPageLoad = false; + } + return pageModule; + }, Theme.NotFound); +} +if (inBrowser) { + createApp().then(({ app, router, data }) => { + router.go().then(() => { + useUpdateHead(router.route, data.site); + app.mount("#app"); + }); + }); +} +export { + createApp +}; diff --git a/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DDvkKcys.js b/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DDvkKcys.js new file mode 100644 index 0000000..3012155 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DDvkKcys.js @@ -0,0 +1,4 @@ +const _localSearchIndexroot = '{"documentCount":22,"nextId":22,"documentIds":{"0":"/ai-coding-kit/#quick-start","1":"/ai-coding-kit/#or-install-via-package-manager","2":"/ai-coding-kit/#platform-support","3":"/ai-coding-kit/#modules","4":"/ai-coding-kit/ios-engineer/#ios-engineer","5":"/ai-coding-kit/ios-engineer/#architecture","6":"/ai-coding-kit/ios-engineer/#rule-system","7":"/ai-coding-kit/ios-engineer/#key-rules","8":"/ai-coding-kit/ios-engineer/#ir-001-—-language-anchoring","9":"/ai-coding-kit/ios-engineer/#ir-006-—-version-context-block","10":"/ai-coding-kit/ios-engineer/#ir-011-—-cognitive-adversary-mode","11":"/ai-coding-kit/ios-engineer/#evolution-governance","12":"/ai-coding-kit/ios-engineer/rule-index#rule-index","13":"/ai-coding-kit/ios-engineer/rule-index#iron-rules-ir-nnn","14":"/ai-coding-kit/ios-engineer/rule-index#global-rules-gr-nnn","15":"/ai-coding-kit/ios-engineer/rule-index#symptom-routing-sym-nnn","16":"/ai-coding-kit/ios-engineer/rule-index#task-routing-route-nnn","17":"/ai-coding-kit/ios-engineer/rule-index#output-templates-out-nnn","18":"/ai-coding-kit/ios-engineer/references#references","19":"/ai-coding-kit/ios-engineer/references#governance-layer","20":"/ai-coding-kit/ios-engineer/references#domain-references","21":"/ai-coding-kit/ios-engineer/references#validation-scripts"},"fieldIds":{"title":0,"titles":1,"text":2},"fieldLength":{"0":[2,1,36],"1":[5,2,13],"2":[2,1,39],"3":[1,1,50],"4":[2,1,51],"5":[1,2,68],"6":[2,2,57],"7":[2,2,1],"8":[4,4,12],"9":[5,4,16],"10":[5,4,21],"11":[2,2,50],"12":[2,1,30],"13":[5,2,35],"14":[5,2,83],"15":[5,2,54],"16":[5,2,24],"17":[5,2,30],"18":[1,1,43],"19":[2,1,26],"20":[2,1,43],"21":[2,1,52]},"averageFieldLength":[3.0454545454545454,1.8636363636363635,37.90909090909091],"storedFields":{"0":{"title":"Quick Start","titles":[]},"1":{"title":"Or install via package manager","titles":["Quick Start"]},"2":{"title":"Platform Support","titles":[]},"3":{"title":"Modules","titles":[]},"4":{"title":"iOS Engineer","titles":[]},"5":{"title":"Architecture","titles":["iOS Engineer"]},"6":{"title":"Rule System","titles":["iOS Engineer"]},"7":{"title":"Key Rules","titles":["iOS Engineer"]},"8":{"title":"IR-001 — Language Anchoring","titles":["iOS Engineer","Key Rules"]},"9":{"title":"IR-006 — Version Context Block","titles":["iOS Engineer","Key Rules"]},"10":{"title":"IR-011 — Cognitive Adversary Mode","titles":["iOS Engineer","Key Rules"]},"11":{"title":"Evolution Governance","titles":["iOS Engineer"]},"12":{"title":"Rule Index","titles":[]},"13":{"title":"Iron Rules (IR-NNN)","titles":["Rule Index"]},"14":{"title":"Global Rules (GR-NNN)","titles":["Rule Index"]},"15":{"title":"Symptom Routing (SYM-NNN)","titles":["Rule Index"]},"16":{"title":"Task Routing (ROUTE-NNN)","titles":["Rule Index"]},"17":{"title":"Output Templates (OUT-NNN)","titles":["Rule Index"]},"18":{"title":"References","titles":[]},"19":{"title":"Governance Layer","titles":["References"]},"20":{"title":"Domain References","titles":["References"]},"21":{"title":"Validation Scripts","titles":["References"]}},"dirtCount":0,"index":[["49",{"2":{"19":1}}],["40+",{"2":{"6":1}}],["jitter",{"2":{"15":1}}],["json",{"2":{"0":3,"2":6}}],["write",{"2":{"15":1}}],["with",{"2":{"14":1,"17":1,"21":1}}],["why",{"2":{"14":1}}],["which",{"2":{"11":1}}],["when",{"2":{"10":1,"14":1}}],["what",{"2":{"2":1}}],["15",{"2":{"14":1}}],["1",{"2":{"14":2}}],["14",{"2":{"11":1,"21":1}}],["10",{"2":{"6":1,"16":1}}],["010",{"2":{"14":1}}],["011",{"0":{"10":1},"2":{"13":1}}],["008",{"2":{"14":1}}],["007",{"2":{"14":1,"15":1}}],["005",{"2":{"14":1,"15":1}}],["004",{"2":{"14":1,"15":1}}],["003",{"2":{"14":1,"15":1}}],["002",{"2":{"14":1,"15":1}}],["006",{"0":{"9":1},"2":{"13":1,"14":1,"15":1}}],["001",{"0":{"8":1},"2":{"13":1,"14":1,"15":1}}],["knowledge",{"2":{"18":1}}],["key",{"0":{"7":1},"1":{"8":1,"9":1,"10":1}}],["kit",{"2":{"0":2,"1":2,"4":1}}],["6",{"2":{"6":1,"17":1}}],["→",{"2":{"6":2,"14":3,"15":1}}],["7",{"2":{"6":1}}],["9",{"2":{"6":1}}],["5",{"2":{"6":1}}],["27",{"2":{"5":1,"21":1}}],["3",{"2":{"6":1,"14":1}}],["31",{"2":{"5":1}}],["34",{"2":{"5":1,"18":1,"20":1}}],["└──",{"2":{"5":4}}],["│",{"2":{"5":7}}],["├──",{"2":{"5":9}}],["lifecycle",{"2":{"18":1}}],["list",{"2":{"15":1}}],["ledger",{"2":{"19":2,"21":2}}],["legacy",{"2":{"15":1}}],["level",{"2":{"10":1}}],["loaded",{"2":{"18":1}}],["logic",{"2":{"6":1,"14":1}}],["locales",{"2":{"4":1}}],["last",{"2":{"21":1}}],["launch",{"2":{"15":1}}],["lag",{"2":{"15":1}}],["layer",{"0":{"19":1},"2":{"5":1}}],["layered",{"2":{"5":1}}],["language",{"0":{"8":1},"2":{"4":1,"8":2,"13":2}}],["zh",{"2":{"4":1,"5":1}}],["简体中文",{"2":{"4":1}}],["ui",{"2":{"15":1}}],["uikit",{"2":{"4":1}}],["unwrap",{"2":{"15":2}}],["universal",{"2":{"3":1}}],["update",{"2":{"11":1}}],["usage",{"2":{"19":2,"21":2}}],["used",{"2":{"18":1}}],["user",{"2":{"8":1,"13":1}}],["us",{"2":{"4":1,"5":1}}],["root",{"2":{"14":1,"15":1,"17":1}}],["route",{"0":{"16":1},"2":{"6":3,"18":1}}],["routing",{"0":{"15":1,"16":1},"2":{"5":1,"6":2,"16":1,"18":2}}],["runtime",{"2":{"18":1}}],["run",{"2":{"11":1}}],["rule",{"0":{"6":1,"12":1},"1":{"13":1,"14":1,"15":1,"16":1,"17":1},"2":{"5":2,"6":2,"11":1,"12":3,"17":1,"19":2,"21":3}}],["rules",{"0":{"7":1,"13":1,"14":1},"1":{"8":1,"9":1,"10":1},"2":{"4":1,"6":2,"18":1,"19":1}}],["release",{"2":{"20":2}}],["records",{"2":{"20":2}}],["record",{"2":{"17":1}}],["request",{"2":{"15":2}}],["requires",{"2":{"11":1}}],["require",{"2":{"9":1}}],["restatement",{"2":{"10":1,"13":1}}],["registry",{"2":{"5":1,"6":1,"12":1,"17":1,"19":1}}],["ref",{"2":{"21":1}}],["refresh",{"2":{"15":1}}],["referenced",{"2":{"12":1}}],["reference",{"2":{"5":1,"14":1,"18":1,"19":1,"20":2,"21":1}}],["references",{"0":{"18":1,"20":1},"1":{"19":1,"20":1,"21":1},"2":{"5":3,"6":2,"11":2,"18":2}}],["refactoring",{"2":{"4":1}}],["review",{"2":{"4":1,"5":1,"16":1,"17":2}}],["renders",{"2":{"3":1}}],["rag",{"2":{"3":2}}],["dates",{"2":{"21":1}}],["data",{"2":{"3":1,"16":1}}],["directory",{"2":{"20":1}}],["diff",{"2":{"14":1}}],["discipline",{"2":{"6":1}}],["driven",{"2":{"5":1,"11":1}}],["domain",{"0":{"20":1},"2":{"5":2,"18":2,"20":3}}],["detailed",{"2":{"18":1}}],["decision",{"2":{"17":1,"20":2}}],["declaration",{"2":{"14":1}}],["dependency",{"2":{"16":1}}],["design",{"2":{"16":1,"17":1}}],["description",{"2":{"3":1,"19":1}}],["defined",{"2":{"12":1}}],["definitions",{"2":{"3":1}}],["development",{"2":{"4":1}}],["debugging",{"2":{"4":1,"16":1}}],["freshness",{"2":{"21":1}}],["full",{"2":{"18":1,"20":1}}],["fear",{"2":{"15":1}}],["four",{"2":{"14":1}}],["force",{"2":{"15":2}}],["forced",{"2":{"8":1}}],["formatting",{"2":{"14":1}}],["formats",{"2":{"3":1,"6":1}}],["for",{"2":{"4":1,"5":1,"6":1,"12":1,"14":1,"17":2,"18":1,"20":1}}],["fix",{"2":{"14":2}}],["first",{"2":{"12":1,"14":1}}],["files",{"2":{"5":1,"18":1,"20":1,"21":2}}],["file",{"2":{"0":1}}],["flip",{"2":{"10":1}}],["falsifiability",{"2":{"13":1}}],["falsifiable",{"2":{"10":1}}],["failures",{"2":{"14":1}}],["failure",{"2":{"10":1,"15":1}}],["fastify",{"2":{"3":1}}],["+",{"2":{"2":1,"3":3,"14":1}}],["against",{"2":{"21":1}}],["agent",{"2":{"2":1,"3":1,"4":1,"18":1}}],["audits",{"2":{"21":1}}],["audit",{"2":{"21":1}}],["auth",{"2":{"15":1}}],["automated",{"2":{"12":1}}],["auto",{"2":{"4":1,"5":1,"6":2,"19":1}}],["app",{"2":{"20":2}}],["api",{"2":{"3":1}}],["at",{"2":{"18":1}}],["amp",{"2":{"16":2,"20":2}}],["add",{"2":{"11":1}}],["adversary",{"0":{"10":1},"2":{"13":1,"19":2}}],["are",{"2":{"11":1,"18":2,"21":1}}],["argument",{"2":{"10":1,"13":1}}],["archived",{"2":{"5":1}}],["archive",{"2":{"5":1,"11":1}}],["architecture",{"0":{"5":1},"2":{"4":1,"16":1,"17":1,"20":4}}],["anti",{"2":{"20":2}}],["and",{"2":{"17":2,"20":2,"21":2}}],["analysis",{"2":{"17":1,"20":2}}],["anchor",{"2":{"17":1}}],["anchors",{"2":{"13":1}}],["anchoring",{"0":{"8":1}}],["an",{"2":{"12":1}}],["answers",{"2":{"9":1}}],["availability",{"2":{"9":1,"13":1}}],["all",{"2":{"9":1,"11":1,"20":1}}],["always",{"2":{"6":1}}],["access",{"2":{"15":1}}],["across",{"2":{"6":1}}],["active",{"2":{"5":1,"13":3,"14":9,"15":7}}],["a",{"2":{"5":1,"9":1,"11":3}}],["async",{"2":{"15":1}}],["assertion",{"2":{"15":1}}],["assumptions",{"2":{"10":1,"13":1}}],["assistant",{"2":{"2":1}}],["as",{"2":{"5":1}}],["ai",{"2":{"0":2,"1":2,"4":2,"18":1}}],["xmcp",{"2":{"3":1}}],["xcode",{"2":{"2":1,"4":1}}],["x26",{"2":{"0":1,"5":1}}],["yaml",{"2":{"2":1}}],["you",{"2":{"0":1}}],["your",{"2":{"0":1,"4":1}}],["verified",{"2":{"21":1}}],["verify",{"2":{"14":1}}],["version",{"0":{"9":1},"2":{"9":1,"13":1}}],["validates",{"2":{"21":2}}],["validate",{"2":{"11":2,"12":1,"21":4}}],["validation",{"0":{"21":1},"2":{"5":1,"12":1,"21":2}}],["variables",{"2":{"2":1}}],["vscode",{"2":{"2":1}}],["via",{"0":{"1":1}}],["memory",{"2":{"15":1}}],["misalignment",{"2":{"15":1}}],["minimal",{"2":{"14":1}}],["mirrors",{"2":{"5":1,"14":1}}],["migration",{"2":{"4":1,"16":1,"17":1}}],["md",{"2":{"5":3,"11":2,"12":1,"17":1,"19":4,"20":9,"21":2}}],["markdown",{"2":{"16":1}}],["max",{"2":{"14":1}}],["matches",{"2":{"4":1,"8":1}}],["manager",{"0":{"1":1}}],["multi",{"2":{"3":1}}],["modify",{"2":{"11":1}}],["modeling",{"2":{"20":2}}],["models",{"2":{"2":1}}],["mode",{"0":{"10":1},"2":{"13":1,"19":2}}],["module",{"2":{"3":1}}],["modules",{"0":{"3":1},"2":{"15":1}}],["mcp",{"2":{"2":4,"3":2}}],["purpose",{"2":{"21":1}}],["push",{"2":{"3":2}}],["plan",{"2":{"17":1}}],["platform",{"0":{"2":1},"2":{"3":2,"6":1,"14":1}}],["persistence",{"2":{"16":1}}],["permission",{"2":{"16":1}}],["performance",{"2":{"4":1}}],["position",{"2":{"10":1}}],["points",{"2":{"17":1}}],["point",{"2":{"5":1}}],["pipeline",{"2":{"5":1,"11":1}}],["provide",{"2":{"18":1}}],["providing",{"2":{"4":1}}],["promote",{"2":{"11":1}}],["propose",{"2":{"11":1}}],["proposals",{"2":{"5":3,"11":1}}],["proposal",{"2":{"5":1,"11":4}}],["production",{"2":{"4":1}}],["project",{"2":{"3":1,"15":1}}],["primary",{"2":{"4":1,"14":1}}],["prevents",{"2":{"14":1}}],["prefix",{"2":{"6":1}}],["pre",{"2":{"3":2,"11":1,"14":1}}],["patterns",{"2":{"20":2}}],["paths",{"2":{"2":1}}],["pagination",{"2":{"15":1}}],["package",{"0":{"1":1}}],["gate",{"2":{"14":1}}],["gated",{"2":{"11":1}}],["gateway",{"2":{"3":2}}],["gr",{"0":{"14":1},"2":{"6":1,"14":9}}],["grade",{"2":{"4":1}}],["global",{"0":{"14":1},"2":{"6":1,"14":1}}],["guard",{"2":{"5":1}}],["guards",{"2":{"3":1}}],["governance",{"0":{"11":1,"19":1},"2":{"5":2,"19":1}}],["governed",{"2":{"3":1}}],["generation",{"2":{"16":1}}],["generated",{"2":{"2":1}}],["gemini",{"2":{"2":1}}],["gets",{"2":{"2":1}}],["g",{"2":{"1":1}}],["githooks",{"2":{"3":1}}],["github",{"2":{"0":1,"20":1}}],["git",{"2":{"0":2,"3":1}}],["naming",{"2":{"15":1}}],["native",{"2":{"3":1}}],["noise",{"2":{"14":1}}],["no",{"2":{"8":1,"14":1}}],["nnn",{"0":{"13":1,"14":1,"15":1,"16":1,"17":1},"2":{"6":5}}],["never",{"2":{"14":1}}],["network",{"2":{"9":1,"13":1,"20":1}}],["networking",{"2":{"4":1,"20":1}}],["need",{"2":{"0":1}}],["npm",{"2":{"1":2}}],["build",{"2":{"16":1,"20":2}}],["budget",{"2":{"14":1}}],["based",{"2":{"18":1}}],["bash",{"2":{"0":2,"1":1}}],["back",{"2":{"15":1}}],["bidirectional",{"2":{"12":1}}],["by",{"2":{"11":1,"14":1,"18":1}}],["between",{"2":{"21":1}}],["before",{"2":{"9":1,"13":1}}],["behavior",{"2":{"9":1}}],["blocks",{"2":{"14":1}}],["block",{"0":{"9":1},"2":{"9":1,"13":1,"14":1}}],["brew",{"2":{"1":1}}],["here",{"2":{"12":1}}],["hidden",{"2":{"10":1,"13":1}}],["how",{"2":{"18":1}}],["hook",{"2":{"11":1}}],["hooks",{"2":{"3":2,"5":1}}],["homebrew",{"2":{"1":1}}],["https",{"2":{"0":1}}],["of",{"2":{"15":1}}],["override",{"2":{"15":1}}],["out",{"0":{"17":1},"2":{"6":1}}],["output",{"0":{"17":1},"2":{"5":1,"6":2,"8":2,"10":1,"13":1,"14":1,"17":1}}],["openai",{"2":{"3":1}}],["organized",{"2":{"5":1}}],["or",{"0":{"1":1},"2":{"11":1,"14":1,"18":1}}],["on",{"2":{"13":1,"18":1,"20":1}}],["one",{"2":{"0":1}}],["only",{"2":{"0":1}}],["$editor",{"2":{"0":1}}],["extensions",{"2":{"20":2}}],["expose",{"2":{"14":1}}],["examples",{"2":{"20":2}}],["example",{"2":{"0":1}}],["epistemic",{"2":{"6":1}}],["etc",{"2":{"3":1}}],["every",{"2":{"12":1}}],["everything",{"2":{"0":1}}],["evolves",{"2":{"11":1}}],["evolution",{"0":{"11":1},"2":{"3":1,"5":7,"11":3,"19":2,"21":3}}],["entries",{"2":{"16":1}}],["entry",{"2":{"5":1}}],["energy",{"2":{"15":1}}],["ensures",{"2":{"12":1,"21":1}}],["enforcement",{"2":{"15":1}}],["enforced",{"2":{"6":1}}],["enforces",{"2":{"6":1}}],["en",{"2":{"4":1,"5":1}}],["english",{"2":{"4":1,"5":1}}],["engineer",{"0":{"4":1},"1":{"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1},"2":{"5":1,"12":1,"14":1,"18":1}}],["engineering",{"2":{"3":1,"4":1,"18":1}}],["engine",{"2":{"3":1}}],["environment",{"2":{"2":1}}],["env",{"2":{"0":3,"3":1}}],["edit",{"2":{"0":1}}],["tracking",{"2":{"19":1}}],["traceable",{"2":{"14":1}}],["triggered",{"2":{"10":1}}],["triggers",{"2":{"5":1}}],["turns",{"2":{"14":1}}],["types",{"2":{"6":1}}],["typescript",{"2":{"3":1}}],["task",{"0":{"16":1},"2":{"6":2,"18":1}}],["tap",{"2":{"1":1}}],["test",{"2":{"17":1}}],["testing",{"2":{"4":1,"16":1}}],["templates",{"0":{"17":1},"2":{"5":1,"6":1,"17":1,"20":2}}],["through",{"2":{"11":1}}],["this",{"2":{"4":1}}],["they",{"2":{"18":1}}],["them",{"2":{"14":1}}],["then",{"2":{"12":1}}],["the",{"2":{"0":1,"4":2,"5":1,"6":3,"8":1,"11":4,"12":1,"14":1,"17":2,"18":3,"20":1,"21":1}}],["toml",{"2":{"2":2}}],["tool",{"2":{"2":1,"14":1}}],["touching",{"2":{"15":1}}],["touch",{"2":{"0":1}}],["to",{"2":{"0":2,"3":1,"11":1,"13":1}}],["slow",{"2":{"15":1}}],["single",{"2":{"14":1}}],["summary",{"2":{"13":1,"14":1,"15":1}}],["supported",{"2":{"4":1}}],["support",{"0":{"2":1}}],["same",{"2":{"11":1}}],["snapshot",{"2":{"11":1,"21":1}}],["snapshots",{"2":{"5":2,"21":1}}],["s",{"2":{"8":1,"13":1}}],["step",{"2":{"11":1,"21":1}}],["strength",{"2":{"14":1}}],["strongest",{"2":{"10":1}}],["structured",{"2":{"6":1}}],["stale",{"2":{"15":1}}],["status",{"2":{"13":1,"14":1,"15":1,"17":1}}],["state",{"2":{"11":1,"15":1,"21":1}}],["staged",{"2":{"11":1}}],["stack",{"2":{"0":1,"1":2}}],["start",{"0":{"0":1},"1":{"1":1}}],["scenario",{"2":{"21":2}}],["scenarios",{"2":{"18":1}}],["script",{"2":{"12":1,"21":1}}],["scripts",{"0":{"21":1},"2":{"5":3,"11":1,"21":2}}],["scope",{"2":{"6":1}}],["specs",{"2":{"21":1}}],["specification",{"2":{"19":1,"21":1}}],["specific",{"2":{"5":1,"18":1}}],["spm",{"2":{"4":1}}],["sym",{"0":{"15":1},"2":{"6":1,"15":7,"18":1}}],["symptoms",{"2":{"6":1}}],["symptom",{"0":{"15":1},"2":{"6":1,"18":1}}],["system",{"0":{"6":1},"2":{"5":1}}],["synced",{"2":{"2":1}}],["sync",{"2":{"0":2,"3":3}}],["swiftui",{"2":{"4":1,"9":1,"13":1}}],["swift",{"2":{"4":1,"18":1}}],["source",{"2":{"3":1}}],["skill",{"2":{"3":1,"4":2,"5":2,"6":1,"11":4,"12":1,"14":1,"18":1,"21":4}}],["skills",{"2":{"2":3,"3":1,"14":1,"16":1}}],["section",{"2":{"14":1}}],["secondary",{"2":{"14":1}}],["security",{"2":{"14":1,"16":1}}],["secrets",{"2":{"0":4,"3":2}}],["see",{"2":{"6":1,"17":1,"20":1}}],["self",{"2":{"5":1,"10":1,"19":1}}],["settings",{"2":{"2":2}}],["ships",{"2":{"21":1}}],["sh",{"2":{"0":1,"11":1,"12":1,"21":6}}],["implement",{"2":{"11":1}}],["implemented",{"2":{"5":1}}],["ir",{"0":{"8":1,"9":1,"10":1,"13":1},"2":{"6":1,"13":3}}],["iron",{"0":{"13":1},"2":{"6":1}}],["i18n",{"2":{"5":1}}],["ids",{"2":{"6":1,"12":1,"19":1,"21":2}}],["id",{"2":{"5":1,"12":2,"13":1,"14":1,"15":1,"19":1}}],["is",{"2":{"4":1,"5":1,"12":1,"14":1}}],["ios",{"0":{"4":1},"1":{"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1},"2":{"4":2,"5":1,"12":1,"14":1,"18":2}}],["integrity",{"2":{"21":1}}],["includes",{"2":{"18":1}}],["indicators",{"2":{"14":1}}],["independent",{"2":{"14":1}}],["index",{"0":{"12":1},"1":{"13":1,"14":1,"15":1,"16":1,"17":1},"2":{"5":1,"6":1,"11":1,"17":1,"19":1,"21":1}}],["insufficient",{"2":{"14":1}}],["install",{"0":{"1":1},"2":{"1":2}}],["info",{"2":{"14":1}}],["input",{"2":{"8":1,"13":1}}],["in",{"2":{"4":1,"5":1,"11":2,"12":1,"21":2}}],["init",{"2":{"3":1}}],["injects",{"2":{"3":1}}],["i",{"2":{"0":1,"1":2}}],["current",{"2":{"21":1}}],["cursor",{"2":{"2":2}}],["ci",{"2":{"16":1,"20":2}}],["chaos",{"2":{"15":3}}],["chain",{"2":{"14":1}}],["change",{"2":{"14":1}}],["changes",{"2":{"11":1}}],["check",{"2":{"10":1,"11":1,"21":1}}],["checks",{"2":{"5":1}}],["chinese",{"2":{"8":1}}],["credentials",{"2":{"14":1}}],["create",{"2":{"11":1}}],["cross",{"2":{"6":1,"14":1}}],["crash",{"2":{"4":1,"15":1}}],["cause",{"2":{"14":2,"15":1,"17":1}}],["carried",{"2":{"14":1}}],["cancellation",{"2":{"9":1}}],["canonical",{"2":{"5":1,"12":1,"17":1,"19":1}}],["category",{"2":{"6":1}}],["categories",{"2":{"6":1}}],["cn",{"2":{"4":1,"5":1}}],["cline",{"2":{"2":1}}],["cli",{"2":{"2":2}}],["claude",{"2":{"2":3}}],["clone",{"2":{"0":2}}],["cp",{"2":{"0":1}}],["cd",{"2":{"0":1}}],["core",{"2":{"16":1}}],["covering",{"2":{"16":1,"18":1}}],["coverage",{"2":{"14":1}}],["cognitive",{"0":{"10":1},"2":{"13":1,"19":2}}],["counter",{"2":{"10":1,"13":1}}],["count",{"2":{"6":1}}],["cocoapods",{"2":{"4":1}}],["consistent",{"2":{"21":1}}],["consistency",{"2":{"5":1,"12":1,"21":1}}],["constraint",{"2":{"15":1}}],["conflicts",{"2":{"15":1}}],["confirmation",{"2":{"14":1}}],["confidence",{"2":{"10":1}}],["configs",{"2":{"3":1}}],["config",{"2":{"2":3,"3":2}}],["configure",{"2":{"0":1}}],["conformity",{"2":{"10":1}}],["conditions",{"2":{"10":2}}],["conclusion",{"2":{"10":1}}],["conclusions",{"2":{"9":1,"13":1}}],["concurrency",{"2":{"4":1,"9":1,"13":1}}],["control",{"2":{"15":1}}],["context",{"0":{"9":1},"2":{"9":1,"13":1}}],["content",{"2":{"3":1}}],["continue",{"2":{"2":2}}],["codex",{"2":{"2":3}}],["code",{"2":{"2":1,"4":1,"14":1,"16":2,"17":1,"20":2}}],["codebuddy",{"2":{"2":2}}],["coding",{"2":{"0":2,"1":2,"2":1,"4":2}}],["compares",{"2":{"21":1}}],["compatible",{"2":{"3":1}}],["comprehensive",{"2":{"21":1}}],["compliance",{"2":{"14":1}}],["complete",{"2":{"6":1,"17":1}}],["commit",{"2":{"3":2,"11":2}}],["command",{"2":{"0":1}}],["com",{"2":{"0":1}}],["quick",{"0":{"0":1},"1":{"1":1}}]],"serializationVersion":2}'; +export { + _localSearchIndexroot as default +}; diff --git a/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.DoLsN_4M.js b/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.DoLsN_4M.js new file mode 100644 index 0000000..38f91d7 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.DoLsN_4M.js @@ -0,0 +1,5343 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +import { V as __vitePreload, q as watch, ah as tryOnScopeDispose, h as computed, ai as toValue, aj as toArray, ak as unrefElement, al as notNullish, G as shallowRef, d as defineComponent, am as computedAsync, p as ref, an as useSessionStorage, ao as useLocalStorage, s as watchEffect, ap as watchDebounced, v as onMounted, P as nextTick, O as onKeyStroke, aq as useRouter, ar as useEventListener, W as useScrollLock, R as inBrowser, $ as onBeforeUnmount, o as openBlock, b as createBlock, j as createBaseVNode, a0 as withModifiers, k as unref, as as withDirectives, at as vModelText, au as isRef, c as createElementBlock, n as normalizeClass, e as createCommentVNode, B as renderList, F as Fragment, a as createTextVNode, t as toDisplayString, av as Teleport, aw as markRaw, ax as createApp, a6 as dataSymbol, ab as pathToFile, ay as escapeRegExp, _ as _export_sfc } from "./framework.BcMzFyCJ.js"; +import { u as useData, c as createSearchTranslate } from "./theme.BrkFYulG.js"; +const localSearchIndex = { "root": () => __vitePreload(() => import("./@localSearchIndexroot.DDvkKcys.js"), true ? [] : void 0) }; +/*! +* tabbable 6.5.0 +* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE +*/ +var candidateSelectors = ["input:not([inert]):not([inert] *)", "select:not([inert]):not([inert] *)", "textarea:not([inert]):not([inert] *)", "a[href]:not([inert]):not([inert] *)", "area[href]:not([inert]):not([inert] *)", "button:not([inert]):not([inert] *)", "[tabindex]:not(slot):not([inert]):not([inert] *)", "audio[controls]:not([inert]):not([inert] *)", "video[controls]:not([inert]):not([inert] *)", '[contenteditable]:not([contenteditable="false"]):not([inert]):not([inert] *)', "details>summary:first-of-type:not([inert]):not([inert] *)", "details:not([inert]):not([inert] *)"]; +var candidateSelector = /* @__PURE__ */ candidateSelectors.join(","); +var NoElement = typeof Element === "undefined"; +var matches = NoElement ? function() { +} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +var getRootNode = !NoElement && Element.prototype.getRootNode ? function(element) { + var _element$getRootNode; + return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element); +} : function(element) { + return element === null || element === void 0 ? void 0 : element.ownerDocument; +}; +var _isInert = function isInert(node, lookUp) { + var _node$getAttribute; + if (lookUp === void 0) { + lookUp = true; + } + var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, "inert"); + var inert = inertAtt === "" || inertAtt === "true"; + var result = inert || lookUp && node && // closest does not exist on shadow roots, so we fall back to a manual + // lookup upward, in case it is not defined. + (typeof node.closest === "function" ? node.closest("[inert]") : _isInert(node.parentNode)); + return result; +}; +var isContentEditable = function isContentEditable2(node) { + var _node$getAttribute2; + var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, "contenteditable"); + return attValue === "" || attValue === "true"; +}; +var getCandidates = function getCandidates2(el, includeContainer, filter) { + if (_isInert(el)) { + return []; + } + var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector)); + if (includeContainer && matches.call(el, candidateSelector)) { + candidates.unshift(el); + } + candidates = candidates.filter(filter); + return candidates; +}; +var _getCandidatesIteratively = function getCandidatesIteratively(elements, includeContainer, options) { + var candidates = []; + var elementsToCheck = Array.from(elements); + while (elementsToCheck.length) { + var element = elementsToCheck.shift(); + if (_isInert(element, false)) { + continue; + } + if (element.tagName === "SLOT") { + var assigned = element.assignedElements(); + var content = assigned.length ? assigned : element.children; + var nestedCandidates = _getCandidatesIteratively(content, true, options); + if (options.flatten) { + candidates.push.apply(candidates, nestedCandidates); + } else { + candidates.push({ + scopeParent: element, + candidates: nestedCandidates + }); + } + } else { + var validCandidate = matches.call(element, candidateSelector); + if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) { + candidates.push(element); + } + var shadowRoot = element.shadowRoot || // check for an undisclosed shadow + typeof options.getShadowRoot === "function" && options.getShadowRoot(element); + var validShadowRoot = !_isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element)); + if (shadowRoot && validShadowRoot) { + var _nestedCandidates = _getCandidatesIteratively(shadowRoot === true ? element.children : shadowRoot.children, true, options); + if (options.flatten) { + candidates.push.apply(candidates, _nestedCandidates); + } else { + candidates.push({ + scopeParent: element, + candidates: _nestedCandidates + }); + } + } else { + elementsToCheck.unshift.apply(elementsToCheck, element.children); + } + } + } + return candidates; +}; +var hasTabIndex = function hasTabIndex2(node) { + return !isNaN(parseInt(node.getAttribute("tabindex"), 10)); +}; +var getTabIndex = function getTabIndex2(node) { + if (!node) { + throw new Error("No node provided"); + } + if (node.tabIndex < 0) { + if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) { + return 0; + } + } + return node.tabIndex; +}; +var getSortOrderTabIndex = function getSortOrderTabIndex2(node, isScope) { + var tabIndex = getTabIndex(node); + if (tabIndex < 0 && isScope && !hasTabIndex(node)) { + return 0; + } + return tabIndex; +}; +var sortOrderedTabbables = function sortOrderedTabbables2(a, b) { + return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex; +}; +var isInput = function isInput2(node) { + return node.tagName === "INPUT"; +}; +var isHiddenInput = function isHiddenInput2(node) { + return isInput(node) && node.type === "hidden"; +}; +var isDetailsWithSummary = function isDetailsWithSummary2(node) { + var r = node.tagName === "DETAILS" && Array.prototype.slice.apply(node.children).some(function(child) { + return child.tagName === "SUMMARY"; + }); + return r; +}; +var getCheckedRadio = function getCheckedRadio2(nodes, form) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].checked && nodes[i].form === form) { + return nodes[i]; + } + } +}; +var isTabbableRadio = function isTabbableRadio2(node) { + if (!node.name) { + return true; + } + var radioScope = node.form || getRootNode(node); + var queryRadios = function queryRadios2(name) { + return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]'); + }; + var radioSet; + if (typeof window !== "undefined" && typeof window.CSS !== "undefined" && typeof window.CSS.escape === "function") { + radioSet = queryRadios(window.CSS.escape(node.name)); + } else { + try { + radioSet = queryRadios(node.name); + } catch (err) { + console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s", err.message); + return false; + } + } + var checked = getCheckedRadio(radioSet, node.form); + return !checked || checked === node; +}; +var isRadio = function isRadio2(node) { + return isInput(node) && node.type === "radio"; +}; +var isNonTabbableRadio = function isNonTabbableRadio2(node) { + return isRadio(node) && !isTabbableRadio(node); +}; +var isNodeAttached = function isNodeAttached2(node) { + var _nodeRoot; + var nodeRoot = node && getRootNode(node); + var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host; + var attached = false; + if (nodeRoot && nodeRoot !== node) { + var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument; + attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node)); + while (!attached && nodeRootHost) { + var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD; + nodeRoot = getRootNode(nodeRootHost); + nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host; + attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost)); + } + } + return attached; +}; +var isZeroArea = function isZeroArea2(node) { + var _node$getBoundingClie = node.getBoundingClientRect(), width = _node$getBoundingClie.width, height = _node$getBoundingClie.height; + return width === 0 && height === 0; +}; +var isHidden = function isHidden2(node, _ref) { + var displayCheck = _ref.displayCheck, getShadowRoot = _ref.getShadowRoot; + if (displayCheck === "full-native") { + if ("checkVisibility" in node) { + var visible = node.checkVisibility({ + // Checking opacity might be desirable for some use cases, but natively, + // opacity zero elements _are_ focusable and tabbable. + checkOpacity: false, + opacityProperty: false, + contentVisibilityAuto: true, + visibilityProperty: true, + // This is an alias for `visibilityProperty`. Contemporary browsers + // support both. However, this alias has wider browser support (Chrome + // >= 105 and Firefox >= 106, vs. Chrome >= 121 and Firefox >= 122), so + // we include it anyway. + checkVisibilityCSS: true + }); + return !visible; + } + } + var _getComputedStyle = getComputedStyle(node), visibility = _getComputedStyle.visibility; + if (visibility === "hidden" || visibility === "collapse") { + return true; + } + var isDirectSummary = matches.call(node, "details>summary:first-of-type"); + var nodeUnderDetails = isDirectSummary ? node.parentElement : node; + if (matches.call(nodeUnderDetails, "details:not([open]) *")) { + return true; + } + if (!displayCheck || displayCheck === "full" || // full-native can run this branch when it falls through in case + // Element#checkVisibility is unsupported + displayCheck === "full-native" || displayCheck === "legacy-full") { + if (typeof getShadowRoot === "function") { + var originalNode = node; + while (node) { + var parentElement = node.parentElement; + var rootNode = getRootNode(node); + if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true) { + return isZeroArea(node); + } else if (node.assignedSlot) { + node = node.assignedSlot; + } else if (!parentElement && rootNode !== node.ownerDocument) { + node = rootNode.host; + } else { + node = parentElement; + } + } + node = originalNode; + } + if (isNodeAttached(node)) { + return !node.getClientRects().length; + } + if (displayCheck !== "legacy-full") { + return true; + } + } else if (displayCheck === "non-zero-area") { + return isZeroArea(node); + } + return false; +}; +var isDisabledFromFieldset = function isDisabledFromFieldset2(node) { + if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) { + var parentNode = node.parentElement; + while (parentNode) { + if (parentNode.tagName === "FIELDSET" && parentNode.disabled) { + for (var i = 0; i < parentNode.children.length; i++) { + var child = parentNode.children.item(i); + if (child.tagName === "LEGEND") { + return matches.call(parentNode, "fieldset[disabled] *") ? true : !child.contains(node); + } + } + return true; + } + parentNode = parentNode.parentElement; + } + } + return false; +}; +var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable2(options, node) { + if (node.disabled || isHiddenInput(node) || isHidden(node, options) || // For a details element with a summary, the summary element gets the focus + isDetailsWithSummary(node) || isDisabledFromFieldset(node)) { + return false; + } + return true; +}; +var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable2(options, node) { + if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) { + return false; + } + return true; +}; +var isShadowRootTabbable = function isShadowRootTabbable2(shadowHostNode) { + var tabIndex = parseInt(shadowHostNode.getAttribute("tabindex"), 10); + if (isNaN(tabIndex) || tabIndex >= 0) { + return true; + } + return false; +}; +var _sortByOrder = function sortByOrder(candidates) { + var regularTabbables = []; + var orderedTabbables = []; + candidates.forEach(function(item, i) { + var isScope = !!item.scopeParent; + var element = isScope ? item.scopeParent : item; + var candidateTabindex = getSortOrderTabIndex(element, isScope); + var elements = isScope ? _sortByOrder(item.candidates) : element; + if (candidateTabindex === 0) { + isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element); + } else { + orderedTabbables.push({ + documentOrder: i, + tabIndex: candidateTabindex, + item, + isScope, + content: elements + }); + } + }); + return orderedTabbables.sort(sortOrderedTabbables).reduce(function(acc, sortable) { + sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content); + return acc; + }, []).concat(regularTabbables); +}; +var tabbable = function tabbable2(container, options) { + options = options || {}; + var candidates; + if (options.getShadowRoot) { + candidates = _getCandidatesIteratively([container], options.includeContainer, { + filter: isNodeMatchingSelectorTabbable.bind(null, options), + flatten: false, + getShadowRoot: options.getShadowRoot, + shadowRootFilter: isShadowRootTabbable + }); + } else { + candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options)); + } + return _sortByOrder(candidates); +}; +var focusable = function focusable2(container, options) { + options = options || {}; + var candidates; + if (options.getShadowRoot) { + candidates = _getCandidatesIteratively([container], options.includeContainer, { + filter: isNodeMatchingSelectorFocusable.bind(null, options), + flatten: true, + getShadowRoot: options.getShadowRoot + }); + } else { + candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options)); + } + return candidates; +}; +var isTabbable = function isTabbable2(node, options) { + options = options || {}; + if (!node) { + throw new Error("No node provided"); + } + if (matches.call(node, candidateSelector) === false) { + return false; + } + return isNodeMatchingSelectorTabbable(options, node); +}; +var focusableCandidateSelector = /* @__PURE__ */ candidateSelectors.concat("iframe:not([inert]):not([inert] *)").join(","); +var isFocusable = function isFocusable2(node, options) { + options = options || {}; + if (!node) { + throw new Error("No node provided"); + } + if (matches.call(node, focusableCandidateSelector) === false) { + return false; + } + return isNodeMatchingSelectorFocusable(options, node); +}; +/*! +* focus-trap 7.8.0 +* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE +*/ +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _arrayWithoutHoles(r) { + if (Array.isArray(r)) return _arrayLikeToArray(r); +} +function _createForOfIteratorHelper(r, e) { + var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; + if (!t) { + if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) { + t && (r = t); + var n = 0, F = function() { + }; + return { + s: F, + n: function() { + return n >= r.length ? { + done: true + } : { + done: false, + value: r[n++] + }; + }, + e: function(r2) { + throw r2; + }, + f: F + }; + } + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + var o, a = true, u = false; + return { + s: function() { + t = t.call(r); + }, + n: function() { + var r2 = t.next(); + return a = r2.done, r2; + }, + e: function(r2) { + u = true, o = r2; + }, + f: function() { + try { + a || null == t.return || t.return(); + } finally { + if (u) throw o; + } + } + }; +} +function _defineProperty(e, r, t) { + return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { + value: t, + enumerable: true, + configurable: true, + writable: true + }) : e[r] = t, e; +} +function _iterableToArray(r) { + if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); +} +function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && (o = o.filter(function(r2) { + return Object.getOwnPropertyDescriptor(e, r2).enumerable; + })), t.push.apply(t, o); + } + return t; +} +function _objectSpread2(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 ? ownKeys(Object(t), true).forEach(function(r2) { + _defineProperty(e, r2, t[r2]); + }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r2) { + Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2)); + }); + } + return e; +} +function _toConsumableArray(r) { + return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); +} +function _toPrimitive(t, r) { + if ("object" != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r); + if ("object" != typeof i) return i; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === r ? String : Number)(t); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, "string"); + return "symbol" == typeof i ? i : i + ""; +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ("string" == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; + } +} +var activeFocusTraps = { + // Returns the trap from the top of the stack. + getActiveTrap: function getActiveTrap(trapStack) { + if ((trapStack === null || trapStack === void 0 ? void 0 : trapStack.length) > 0) { + return trapStack[trapStack.length - 1]; + } + return null; + }, + // Pauses the currently active trap, then adds a new trap to the stack. + activateTrap: function activateTrap(trapStack, trap) { + var activeTrap = activeFocusTraps.getActiveTrap(trapStack); + if (trap !== activeTrap) { + activeFocusTraps.pauseTrap(trapStack); + } + var trapIndex = trapStack.indexOf(trap); + if (trapIndex === -1) { + trapStack.push(trap); + } else { + trapStack.splice(trapIndex, 1); + trapStack.push(trap); + } + }, + // Removes the trap from the top of the stack, then unpauses the next trap down. + deactivateTrap: function deactivateTrap(trapStack, trap) { + var trapIndex = trapStack.indexOf(trap); + if (trapIndex !== -1) { + trapStack.splice(trapIndex, 1); + } + activeFocusTraps.unpauseTrap(trapStack); + }, + // Pauses the trap at the top of the stack. + pauseTrap: function pauseTrap(trapStack) { + var activeTrap = activeFocusTraps.getActiveTrap(trapStack); + activeTrap === null || activeTrap === void 0 || activeTrap._setPausedState(true); + }, + // Unpauses the trap at the top of the stack. + unpauseTrap: function unpauseTrap(trapStack) { + var activeTrap = activeFocusTraps.getActiveTrap(trapStack); + if (activeTrap && !activeTrap._isManuallyPaused()) { + activeTrap._setPausedState(false); + } + } +}; +var isSelectableInput = function isSelectableInput2(node) { + return node.tagName && node.tagName.toLowerCase() === "input" && typeof node.select === "function"; +}; +var isEscapeEvent = function isEscapeEvent2(e) { + return (e === null || e === void 0 ? void 0 : e.key) === "Escape" || (e === null || e === void 0 ? void 0 : e.key) === "Esc" || (e === null || e === void 0 ? void 0 : e.keyCode) === 27; +}; +var isTabEvent = function isTabEvent2(e) { + return (e === null || e === void 0 ? void 0 : e.key) === "Tab" || (e === null || e === void 0 ? void 0 : e.keyCode) === 9; +}; +var isKeyForward = function isKeyForward2(e) { + return isTabEvent(e) && !e.shiftKey; +}; +var isKeyBackward = function isKeyBackward2(e) { + return isTabEvent(e) && e.shiftKey; +}; +var delay = function delay2(fn) { + return setTimeout(fn, 0); +}; +var valueOrHandler = function valueOrHandler2(value) { + for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + params[_key - 1] = arguments[_key]; + } + return typeof value === "function" ? value.apply(void 0, params) : value; +}; +var getActualTarget = function getActualTarget2(event) { + return event.target.shadowRoot && typeof event.composedPath === "function" ? event.composedPath()[0] : event.target; +}; +var internalTrapStack = []; +var createFocusTrap = function createFocusTrap2(elements, userOptions) { + var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document; + var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack; + var config = _objectSpread2({ + returnFocusOnDeactivate: true, + escapeDeactivates: true, + delayInitialFocus: true, + isolateSubtrees: false, + isKeyForward, + isKeyBackward + }, userOptions); + var state = { + // containers given to createFocusTrap() + /** @type {Array} */ + containers: [], + // list of objects identifying tabbable nodes in `containers` in the trap + // NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap + // is active, but the trap should never get to a state where there isn't at least one group + // with at least one tabbable node in it (that would lead to an error condition that would + // result in an error being thrown) + /** @type {Array<{ + * container: HTMLElement, + * tabbableNodes: Array, // empty if none + * focusableNodes: Array, // empty if none + * posTabIndexesFound: boolean, + * firstTabbableNode: HTMLElement|undefined, + * lastTabbableNode: HTMLElement|undefined, + * firstDomTabbableNode: HTMLElement|undefined, + * lastDomTabbableNode: HTMLElement|undefined, + * nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined + * }>} + */ + containerGroups: [], + // same order/length as `containers` list + // references to objects in `containerGroups`, but only those that actually have + // tabbable nodes in them + // NOTE: same order as `containers` and `containerGroups`, but __not necessarily__ + // the same length + tabbableGroups: [], + // references to nodes that are siblings to the ancestors of this trap's containers. + /** @type {Set} */ + adjacentElements: /* @__PURE__ */ new Set(), + // references to nodes that were inert or aria-hidden before the trap was activated. + /** @type {Set} */ + alreadySilent: /* @__PURE__ */ new Set(), + nodeFocusedBeforeActivation: null, + mostRecentlyFocusedNode: null, + active: false, + paused: false, + manuallyPaused: false, + // timer ID for when delayInitialFocus is true and initial focus in this trap + // has been delayed during activation + delayInitialFocusTimer: void 0, + // the most recent KeyboardEvent for the configured nav key (typically [SHIFT+]TAB), if any + recentNavEvent: void 0 + }; + var trap; + var getOption = function getOption2(configOverrideOptions, optionName, configOptionName) { + return configOverrideOptions && configOverrideOptions[optionName] !== void 0 ? configOverrideOptions[optionName] : config[configOptionName || optionName]; + }; + var findContainerIndex = function findContainerIndex2(element, event) { + var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === "function" ? event.composedPath() : void 0; + return state.containerGroups.findIndex(function(_ref) { + var container = _ref.container, tabbableNodes = _ref.tabbableNodes; + return container.contains(element) || // fall back to explicit tabbable search which will take into consideration any + // web components if the `tabbableOptions.getShadowRoot` option was used for + // the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't + // look inside web components even if open) + (composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function(node) { + return node === element; + }); + }); + }; + var getNodeForOption = function getNodeForOption2(optionName) { + var _ref2 = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, _ref2$hasFallback = _ref2.hasFallback, hasFallback = _ref2$hasFallback === void 0 ? false : _ref2$hasFallback, _ref2$params = _ref2.params, params = _ref2$params === void 0 ? [] : _ref2$params; + var optionValue = config[optionName]; + if (typeof optionValue === "function") { + optionValue = optionValue.apply(void 0, _toConsumableArray(params)); + } + if (optionValue === true) { + optionValue = void 0; + } + if (!optionValue) { + if (optionValue === void 0 || optionValue === false) { + return optionValue; + } + throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node")); + } + var node = optionValue; + if (typeof optionValue === "string") { + try { + node = doc.querySelector(optionValue); + } catch (err) { + throw new Error("`".concat(optionName, '` appears to be an invalid selector; error="').concat(err.message, '"')); + } + if (!node) { + if (!hasFallback) { + throw new Error("`".concat(optionName, "` as selector refers to no known node")); + } + } + } + return node; + }; + var getInitialFocusNode = function getInitialFocusNode2() { + var node = getNodeForOption("initialFocus", { + hasFallback: true + }); + if (node === false) { + return false; + } + if (node === void 0 || node && !isFocusable(node, config.tabbableOptions)) { + if (findContainerIndex(doc.activeElement) >= 0) { + node = doc.activeElement; + } else { + var firstTabbableGroup = state.tabbableGroups[0]; + var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode; + node = firstTabbableNode || getNodeForOption("fallbackFocus"); + } + } else if (node === null) { + node = getNodeForOption("fallbackFocus"); + } + if (!node) { + throw new Error("Your focus-trap needs to have at least one focusable element"); + } + return node; + }; + var updateTabbableNodes = function updateTabbableNodes2() { + state.containerGroups = state.containers.map(function(container) { + var tabbableNodes = tabbable(container, config.tabbableOptions); + var focusableNodes = focusable(container, config.tabbableOptions); + var firstTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[0] : void 0; + var lastTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : void 0; + var firstDomTabbableNode = focusableNodes.find(function(node) { + return isTabbable(node); + }); + var lastDomTabbableNode = focusableNodes.slice().reverse().find(function(node) { + return isTabbable(node); + }); + var posTabIndexesFound = !!tabbableNodes.find(function(node) { + return getTabIndex(node) > 0; + }); + return { + container, + tabbableNodes, + focusableNodes, + /** True if at least one node with positive `tabindex` was found in this container. */ + posTabIndexesFound, + /** First tabbable node in container, __tabindex__ order; `undefined` if none. */ + firstTabbableNode, + /** Last tabbable node in container, __tabindex__ order; `undefined` if none. */ + lastTabbableNode, + // NOTE: DOM order is NOT NECESSARILY "document position" order, but figuring that out + // would require more than just https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + // because that API doesn't work with Shadow DOM as well as it should (@see + // https://github.com/whatwg/dom/issues/320) and since this first/last is only needed, so far, + // to address an edge case related to positive tabindex support, this seems like a much easier, + // "close enough most of the time" alternative for positive tabindexes which should generally + // be avoided anyway... + /** First tabbable node in container, __DOM__ order; `undefined` if none. */ + firstDomTabbableNode, + /** Last tabbable node in container, __DOM__ order; `undefined` if none. */ + lastDomTabbableNode, + /** + * Finds the __tabbable__ node that follows the given node in the specified direction, + * in this container, if any. + * @param {HTMLElement} node + * @param {boolean} [forward] True if going in forward tab order; false if going + * in reverse. + * @returns {HTMLElement|undefined} The next tabbable node, if any. + */ + nextTabbableNode: function nextTabbableNode(node) { + var forward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : true; + var nodeIdx = tabbableNodes.indexOf(node); + if (nodeIdx < 0) { + if (forward) { + return focusableNodes.slice(focusableNodes.indexOf(node) + 1).find(function(el) { + return isTabbable(el); + }); + } + return focusableNodes.slice(0, focusableNodes.indexOf(node)).reverse().find(function(el) { + return isTabbable(el); + }); + } + return tabbableNodes[nodeIdx + (forward ? 1 : -1)]; + } + }; + }); + state.tabbableGroups = state.containerGroups.filter(function(group) { + return group.tabbableNodes.length > 0; + }); + if (state.tabbableGroups.length <= 0 && !getNodeForOption("fallbackFocus")) { + throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times"); + } + if (state.containerGroups.find(function(g) { + return g.posTabIndexesFound; + }) && state.containerGroups.length > 1) { + throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps."); + } + }; + var _getActiveElement = function getActiveElement(el) { + var activeElement = el.activeElement; + if (!activeElement) { + return; + } + if (activeElement.shadowRoot && activeElement.shadowRoot.activeElement !== null) { + return _getActiveElement(activeElement.shadowRoot); + } + return activeElement; + }; + var _tryFocus = function tryFocus(node) { + if (node === false) { + return; + } + if (node === _getActiveElement(document)) { + return; + } + if (!node || !node.focus) { + _tryFocus(getInitialFocusNode()); + return; + } + node.focus({ + preventScroll: !!config.preventScroll + }); + state.mostRecentlyFocusedNode = node; + if (isSelectableInput(node)) { + node.select(); + } + }; + var getReturnFocusNode = function getReturnFocusNode2(previousActiveElement) { + var node = getNodeForOption("setReturnFocus", { + params: [previousActiveElement] + }); + return node ? node : node === false ? false : previousActiveElement; + }; + var findNextNavNode = function findNextNavNode2(_ref3) { + var target = _ref3.target, event = _ref3.event, _ref3$isBackward = _ref3.isBackward, isBackward = _ref3$isBackward === void 0 ? false : _ref3$isBackward; + target = target || getActualTarget(event); + updateTabbableNodes(); + var destinationNode = null; + if (state.tabbableGroups.length > 0) { + var containerIndex = findContainerIndex(target, event); + var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : void 0; + if (containerIndex < 0) { + if (isBackward) { + destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode; + } else { + destinationNode = state.tabbableGroups[0].firstTabbableNode; + } + } else if (isBackward) { + var startOfGroupIndex = state.tabbableGroups.findIndex(function(_ref4) { + var firstTabbableNode = _ref4.firstTabbableNode; + return target === firstTabbableNode; + }); + if (startOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) { + startOfGroupIndex = containerIndex; + } + if (startOfGroupIndex >= 0) { + var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1; + var destinationGroup = state.tabbableGroups[destinationGroupIndex]; + destinationNode = getTabIndex(target) >= 0 ? destinationGroup.lastTabbableNode : destinationGroup.lastDomTabbableNode; + } else if (!isTabEvent(event)) { + destinationNode = containerGroup.nextTabbableNode(target, false); + } + } else { + var lastOfGroupIndex = state.tabbableGroups.findIndex(function(_ref5) { + var lastTabbableNode = _ref5.lastTabbableNode; + return target === lastTabbableNode; + }); + if (lastOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) { + lastOfGroupIndex = containerIndex; + } + if (lastOfGroupIndex >= 0) { + var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1; + var _destinationGroup = state.tabbableGroups[_destinationGroupIndex]; + destinationNode = getTabIndex(target) >= 0 ? _destinationGroup.firstTabbableNode : _destinationGroup.firstDomTabbableNode; + } else if (!isTabEvent(event)) { + destinationNode = containerGroup.nextTabbableNode(target); + } + } + } else { + destinationNode = getNodeForOption("fallbackFocus"); + } + return destinationNode; + }; + var checkPointerDown = function checkPointerDown2(e) { + var target = getActualTarget(e); + if (findContainerIndex(target, e) >= 0) { + return; + } + if (valueOrHandler(config.clickOutsideDeactivates, e)) { + trap.deactivate({ + // NOTE: by setting `returnFocus: false`, deactivate() will do nothing, + // which will result in the outside click setting focus to the node + // that was clicked (and if not focusable, to "nothing"); by setting + // `returnFocus: true`, we'll attempt to re-focus the node originally-focused + // on activation (or the configured `setReturnFocus` node), whether the + // outside click was on a focusable node or not + returnFocus: config.returnFocusOnDeactivate + }); + return; + } + if (valueOrHandler(config.allowOutsideClick, e)) { + return; + } + e.preventDefault(); + }; + var checkFocusIn = function checkFocusIn2(event) { + var target = getActualTarget(event); + var targetContained = findContainerIndex(target, event) >= 0; + if (targetContained || target instanceof Document) { + if (targetContained) { + state.mostRecentlyFocusedNode = target; + } + } else { + event.stopImmediatePropagation(); + var nextNode; + var navAcrossContainers = true; + if (state.mostRecentlyFocusedNode) { + if (getTabIndex(state.mostRecentlyFocusedNode) > 0) { + var mruContainerIdx = findContainerIndex(state.mostRecentlyFocusedNode); + var tabbableNodes = state.containerGroups[mruContainerIdx].tabbableNodes; + if (tabbableNodes.length > 0) { + var mruTabIdx = tabbableNodes.findIndex(function(node) { + return node === state.mostRecentlyFocusedNode; + }); + if (mruTabIdx >= 0) { + if (config.isKeyForward(state.recentNavEvent)) { + if (mruTabIdx + 1 < tabbableNodes.length) { + nextNode = tabbableNodes[mruTabIdx + 1]; + navAcrossContainers = false; + } + } else { + if (mruTabIdx - 1 >= 0) { + nextNode = tabbableNodes[mruTabIdx - 1]; + navAcrossContainers = false; + } + } + } + } + } else { + if (!state.containerGroups.some(function(g) { + return g.tabbableNodes.some(function(n) { + return getTabIndex(n) > 0; + }); + })) { + navAcrossContainers = false; + } + } + } else { + navAcrossContainers = false; + } + if (navAcrossContainers) { + nextNode = findNextNavNode({ + // move FROM the MRU node, not event-related node (which will be the node that is + // outside the trap causing the focus escape we're trying to fix) + target: state.mostRecentlyFocusedNode, + isBackward: config.isKeyBackward(state.recentNavEvent) + }); + } + if (nextNode) { + _tryFocus(nextNode); + } else { + _tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode()); + } + } + state.recentNavEvent = void 0; + }; + var checkKeyNav = function checkKeyNav2(event) { + var isBackward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; + state.recentNavEvent = event; + var destinationNode = findNextNavNode({ + event, + isBackward + }); + if (destinationNode) { + if (isTabEvent(event)) { + event.preventDefault(); + } + _tryFocus(destinationNode); + } + }; + var checkTabKey = function checkTabKey2(event) { + if (config.isKeyForward(event) || config.isKeyBackward(event)) { + checkKeyNav(event, config.isKeyBackward(event)); + } + }; + var checkEscapeKey = function checkEscapeKey2(event) { + if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) { + event.preventDefault(); + trap.deactivate(); + } + }; + var checkClick = function checkClick2(e) { + var target = getActualTarget(e); + if (findContainerIndex(target, e) >= 0) { + return; + } + if (valueOrHandler(config.clickOutsideDeactivates, e)) { + return; + } + if (valueOrHandler(config.allowOutsideClick, e)) { + return; + } + e.preventDefault(); + e.stopImmediatePropagation(); + }; + var addListeners = function addListeners2() { + if (!state.active) { + return; + } + activeFocusTraps.activateTrap(trapStack, trap); + state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function() { + _tryFocus(getInitialFocusNode()); + }) : _tryFocus(getInitialFocusNode()); + doc.addEventListener("focusin", checkFocusIn, true); + doc.addEventListener("mousedown", checkPointerDown, { + capture: true, + passive: false + }); + doc.addEventListener("touchstart", checkPointerDown, { + capture: true, + passive: false + }); + doc.addEventListener("click", checkClick, { + capture: true, + passive: false + }); + doc.addEventListener("keydown", checkTabKey, { + capture: true, + passive: false + }); + doc.addEventListener("keydown", checkEscapeKey); + return trap; + }; + var collectAdjacentElements = function collectAdjacentElements2(containers) { + if (state.active && !state.paused) { + trap._setSubtreeIsolation(false); + } + state.adjacentElements.clear(); + state.alreadySilent.clear(); + var containerAncestors = /* @__PURE__ */ new Set(); + var adjacentElements = /* @__PURE__ */ new Set(); + var _iterator = _createForOfIteratorHelper(containers), _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done; ) { + var container = _step.value; + containerAncestors.add(container); + var insideShadowRoot = typeof ShadowRoot !== "undefined" && container.getRootNode() instanceof ShadowRoot; + var current = container; + while (current) { + containerAncestors.add(current); + var parent = current.parentElement; + var siblings = []; + if (parent) { + siblings = parent.children; + } else if (!parent && insideShadowRoot) { + siblings = current.getRootNode().children; + parent = current.getRootNode().host; + insideShadowRoot = typeof ShadowRoot !== "undefined" && parent.getRootNode() instanceof ShadowRoot; + } + var _iterator2 = _createForOfIteratorHelper(siblings), _step2; + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done; ) { + var child = _step2.value; + adjacentElements.add(child); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + current = parent; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + containerAncestors.forEach(function(el) { + adjacentElements["delete"](el); + }); + state.adjacentElements = adjacentElements; + }; + var removeListeners = function removeListeners2() { + if (!state.active) { + return; + } + doc.removeEventListener("focusin", checkFocusIn, true); + doc.removeEventListener("mousedown", checkPointerDown, true); + doc.removeEventListener("touchstart", checkPointerDown, true); + doc.removeEventListener("click", checkClick, true); + doc.removeEventListener("keydown", checkTabKey, true); + doc.removeEventListener("keydown", checkEscapeKey); + return trap; + }; + var checkDomRemoval = function checkDomRemoval2(mutations) { + var isFocusedNodeRemoved = mutations.some(function(mutation) { + var removedNodes = Array.from(mutation.removedNodes); + return removedNodes.some(function(node) { + return node === state.mostRecentlyFocusedNode; + }); + }); + if (isFocusedNodeRemoved) { + _tryFocus(getInitialFocusNode()); + } + }; + var mutationObserver = typeof window !== "undefined" && "MutationObserver" in window ? new MutationObserver(checkDomRemoval) : void 0; + var updateObservedNodes = function updateObservedNodes2() { + if (!mutationObserver) { + return; + } + mutationObserver.disconnect(); + if (state.active && !state.paused) { + state.containers.map(function(container) { + mutationObserver.observe(container, { + subtree: true, + childList: true + }); + }); + } + }; + trap = { + get active() { + return state.active; + }, + get paused() { + return state.paused; + }, + activate: function activate(activateOptions) { + if (state.active) { + return this; + } + var onActivate = getOption(activateOptions, "onActivate"); + var onPostActivate = getOption(activateOptions, "onPostActivate"); + var checkCanFocusTrap = getOption(activateOptions, "checkCanFocusTrap"); + var preexistingTrap = activeFocusTraps.getActiveTrap(trapStack); + var revertState = false; + if (preexistingTrap && !preexistingTrap.paused) { + var _preexistingTrap$_set; + (_preexistingTrap$_set = preexistingTrap._setSubtreeIsolation) === null || _preexistingTrap$_set === void 0 || _preexistingTrap$_set.call(preexistingTrap, false); + revertState = true; + } + try { + if (!checkCanFocusTrap) { + updateTabbableNodes(); + } + state.active = true; + state.paused = false; + state.nodeFocusedBeforeActivation = _getActiveElement(doc); + onActivate === null || onActivate === void 0 || onActivate(); + var finishActivation = function finishActivation2() { + if (checkCanFocusTrap) { + updateTabbableNodes(); + } + addListeners(); + updateObservedNodes(); + if (config.isolateSubtrees) { + trap._setSubtreeIsolation(true); + } + onPostActivate === null || onPostActivate === void 0 || onPostActivate(); + }; + if (checkCanFocusTrap) { + checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation); + return this; + } + finishActivation(); + } catch (error) { + if (preexistingTrap === activeFocusTraps.getActiveTrap(trapStack) && revertState) { + var _preexistingTrap$_set2; + (_preexistingTrap$_set2 = preexistingTrap._setSubtreeIsolation) === null || _preexistingTrap$_set2 === void 0 || _preexistingTrap$_set2.call(preexistingTrap, true); + } + throw error; + } + return this; + }, + deactivate: function deactivate(deactivateOptions) { + if (!state.active) { + return this; + } + var options = _objectSpread2({ + onDeactivate: config.onDeactivate, + onPostDeactivate: config.onPostDeactivate, + checkCanReturnFocus: config.checkCanReturnFocus + }, deactivateOptions); + clearTimeout(state.delayInitialFocusTimer); + state.delayInitialFocusTimer = void 0; + if (!state.paused) { + trap._setSubtreeIsolation(false); + } + state.alreadySilent.clear(); + removeListeners(); + state.active = false; + state.paused = false; + updateObservedNodes(); + activeFocusTraps.deactivateTrap(trapStack, trap); + var onDeactivate = getOption(options, "onDeactivate"); + var onPostDeactivate = getOption(options, "onPostDeactivate"); + var checkCanReturnFocus = getOption(options, "checkCanReturnFocus"); + var returnFocus = getOption(options, "returnFocus", "returnFocusOnDeactivate"); + onDeactivate === null || onDeactivate === void 0 || onDeactivate(); + var finishDeactivation = function finishDeactivation2() { + delay(function() { + if (returnFocus) { + _tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)); + } + onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate(); + }); + }; + if (returnFocus && checkCanReturnFocus) { + checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation); + return this; + } + finishDeactivation(); + return this; + }, + pause: function pause(pauseOptions) { + if (!state.active) { + return this; + } + state.manuallyPaused = true; + return this._setPausedState(true, pauseOptions); + }, + unpause: function unpause(unpauseOptions) { + if (!state.active) { + return this; + } + state.manuallyPaused = false; + if (trapStack[trapStack.length - 1] !== this) { + return this; + } + return this._setPausedState(false, unpauseOptions); + }, + updateContainerElements: function updateContainerElements(containerElements) { + var elementsAsArray = [].concat(containerElements).filter(Boolean); + state.containers = elementsAsArray.map(function(element) { + return typeof element === "string" ? doc.querySelector(element) : element; + }); + if (config.isolateSubtrees) { + collectAdjacentElements(state.containers); + } + if (state.active) { + updateTabbableNodes(); + if (config.isolateSubtrees && !state.paused) { + trap._setSubtreeIsolation(true); + } + } + updateObservedNodes(); + return this; + } + }; + Object.defineProperties(trap, { + _isManuallyPaused: { + value: function value() { + return state.manuallyPaused; + } + }, + _setPausedState: { + value: function value(paused, options) { + if (state.paused === paused) { + return this; + } + state.paused = paused; + if (paused) { + var onPause = getOption(options, "onPause"); + var onPostPause = getOption(options, "onPostPause"); + onPause === null || onPause === void 0 || onPause(); + removeListeners(); + updateObservedNodes(); + trap._setSubtreeIsolation(false); + onPostPause === null || onPostPause === void 0 || onPostPause(); + } else { + var onUnpause = getOption(options, "onUnpause"); + var onPostUnpause = getOption(options, "onPostUnpause"); + onUnpause === null || onUnpause === void 0 || onUnpause(); + trap._setSubtreeIsolation(true); + updateTabbableNodes(); + addListeners(); + updateObservedNodes(); + onPostUnpause === null || onPostUnpause === void 0 || onPostUnpause(); + } + return this; + } + }, + _setSubtreeIsolation: { + value: function value(isEnabled) { + if (config.isolateSubtrees) { + state.adjacentElements.forEach(function(el) { + var _el$getAttribute; + if (isEnabled) { + switch (config.isolateSubtrees) { + case "aria-hidden": + if (el.ariaHidden === "true" || ((_el$getAttribute = el.getAttribute("aria-hidden")) === null || _el$getAttribute === void 0 ? void 0 : _el$getAttribute.toLowerCase()) === "true") { + state.alreadySilent.add(el); + } + el.setAttribute("aria-hidden", "true"); + break; + default: + if (el.inert || el.hasAttribute("inert")) { + state.alreadySilent.add(el); + } + el.setAttribute("inert", true); + break; + } + } else { + if (state.alreadySilent.has(el)) ; + else { + switch (config.isolateSubtrees) { + case "aria-hidden": + el.removeAttribute("aria-hidden"); + break; + default: + el.removeAttribute("inert"); + break; + } + } + } + }); + } + } + } + }); + trap.updateContainerElements(elements); + return trap; +}; +function useFocusTrap(target, options = {}) { + let trap; + const { immediate, ...focusTrapOptions } = options; + const hasFocus = shallowRef(false); + const isPaused = shallowRef(false); + const activate = (opts) => trap && trap.activate(opts); + const deactivate = (opts) => trap && trap.deactivate(opts); + const pause = () => { + if (trap) { + trap.pause(); + isPaused.value = true; + } + }; + const unpause = () => { + if (trap) { + trap.unpause(); + isPaused.value = false; + } + }; + const targets = computed(() => { + const _targets = toValue(target); + return toArray(_targets).map((el) => { + const _el = toValue(el); + return typeof _el === "string" ? _el : unrefElement(_el); + }).filter(notNullish); + }); + watch( + targets, + (els) => { + if (!els.length) + return; + trap = createFocusTrap(els, { + ...focusTrapOptions, + onActivate() { + hasFocus.value = true; + if (options.onActivate) + options.onActivate(); + }, + onDeactivate() { + hasFocus.value = false; + if (options.onDeactivate) + options.onDeactivate(); + } + }); + if (immediate) + activate(); + }, + { flush: "post" } + ); + tryOnScopeDispose(() => deactivate()); + return { + hasFocus, + isPaused, + activate, + deactivate, + pause, + unpause + }; +} +class DOMIterator { + /** + * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM + * element, an array of DOM elements, a NodeList or a selector + * @param {boolean} [iframes=true] - A boolean indicating if iframes should + * be handled + * @param {string[]} [exclude=[]] - An array containing exclusion selectors + * for iframes + * @param {number} [iframesTimeout=5000] - A number indicating the ms to + * wait before an iframe should be skipped, in case the load event isn't + * fired. This also applies if the user is offline and the resource of the + * iframe is online (either by the browsers "offline" mode or because + * there's no internet connection) + */ + constructor(ctx, iframes = true, exclude = [], iframesTimeout = 5e3) { + this.ctx = ctx; + this.iframes = iframes; + this.exclude = exclude; + this.iframesTimeout = iframesTimeout; + } + /** + * Checks if the specified DOM element matches the selector + * @param {HTMLElement} element - The DOM element + * @param {string|string[]} selector - The selector or an array with + * selectors + * @return {boolean} + * @access public + */ + static matches(element, selector) { + const selectors = typeof selector === "string" ? [selector] : selector, fn = element.matches || element.matchesSelector || element.msMatchesSelector || element.mozMatchesSelector || element.oMatchesSelector || element.webkitMatchesSelector; + if (fn) { + let match = false; + selectors.every((sel) => { + if (fn.call(element, sel)) { + match = true; + return false; + } + return true; + }); + return match; + } else { + return false; + } + } + /** + * Returns all contexts filtered by duplicates (even nested) + * @return {HTMLElement[]} - An array containing DOM contexts + * @access protected + */ + getContexts() { + let ctx, filteredCtx = []; + if (typeof this.ctx === "undefined" || !this.ctx) { + ctx = []; + } else if (NodeList.prototype.isPrototypeOf(this.ctx)) { + ctx = Array.prototype.slice.call(this.ctx); + } else if (Array.isArray(this.ctx)) { + ctx = this.ctx; + } else if (typeof this.ctx === "string") { + ctx = Array.prototype.slice.call( + document.querySelectorAll(this.ctx) + ); + } else { + ctx = [this.ctx]; + } + ctx.forEach((ctx2) => { + const isDescendant = filteredCtx.filter((contexts) => { + return contexts.contains(ctx2); + }).length > 0; + if (filteredCtx.indexOf(ctx2) === -1 && !isDescendant) { + filteredCtx.push(ctx2); + } + }); + return filteredCtx; + } + /** + * @callback DOMIterator~getIframeContentsSuccessCallback + * @param {HTMLDocument} contents - The contentDocument of the iframe + */ + /** + * Calls the success callback function with the iframe document. If it can't + * be accessed it calls the error callback function + * @param {HTMLElement} ifr - The iframe DOM element + * @param {DOMIterator~getIframeContentsSuccessCallback} successFn + * @param {function} [errorFn] + * @access protected + */ + getIframeContents(ifr, successFn, errorFn = () => { + }) { + let doc; + try { + const ifrWin = ifr.contentWindow; + doc = ifrWin.document; + if (!ifrWin || !doc) { + throw new Error("iframe inaccessible"); + } + } catch (e) { + errorFn(); + } + if (doc) { + successFn(doc); + } + } + /** + * Checks if an iframe is empty (if about:blank is the shown page) + * @param {HTMLElement} ifr - The iframe DOM element + * @return {boolean} + * @access protected + */ + isIframeBlank(ifr) { + const bl = "about:blank", src = ifr.getAttribute("src").trim(), href = ifr.contentWindow.location.href; + return href === bl && src !== bl && src; + } + /** + * Observes the onload event of an iframe and calls the success callback or + * the error callback if the iframe is inaccessible. If the event isn't + * fired within the specified {@link DOMIterator#iframesTimeout}, then it'll + * call the error callback too + * @param {HTMLElement} ifr - The iframe DOM element + * @param {DOMIterator~getIframeContentsSuccessCallback} successFn + * @param {function} errorFn + * @access protected + */ + observeIframeLoad(ifr, successFn, errorFn) { + let called = false, tout = null; + const listener = () => { + if (called) { + return; + } + called = true; + clearTimeout(tout); + try { + if (!this.isIframeBlank(ifr)) { + ifr.removeEventListener("load", listener); + this.getIframeContents(ifr, successFn, errorFn); + } + } catch (e) { + errorFn(); + } + }; + ifr.addEventListener("load", listener); + tout = setTimeout(listener, this.iframesTimeout); + } + /** + * Callback when the iframe is ready + * @callback DOMIterator~onIframeReadySuccessCallback + * @param {HTMLDocument} contents - The contentDocument of the iframe + */ + /** + * Callback if the iframe can't be accessed + * @callback DOMIterator~onIframeReadyErrorCallback + */ + /** + * Calls the callback if the specified iframe is ready for DOM access + * @param {HTMLElement} ifr - The iframe DOM element + * @param {DOMIterator~onIframeReadySuccessCallback} successFn - Success + * callback + * @param {DOMIterator~onIframeReadyErrorCallback} errorFn - Error callback + * @see {@link http://stackoverflow.com/a/36155560/3894981} for + * background information + * @access protected + */ + onIframeReady(ifr, successFn, errorFn) { + try { + if (ifr.contentWindow.document.readyState === "complete") { + if (this.isIframeBlank(ifr)) { + this.observeIframeLoad(ifr, successFn, errorFn); + } else { + this.getIframeContents(ifr, successFn, errorFn); + } + } else { + this.observeIframeLoad(ifr, successFn, errorFn); + } + } catch (e) { + errorFn(); + } + } + /** + * Callback when all iframes are ready for DOM access + * @callback DOMIterator~waitForIframesDoneCallback + */ + /** + * Iterates over all iframes and calls the done callback when all of them + * are ready for DOM access (including nested ones) + * @param {HTMLElement} ctx - The context DOM element + * @param {DOMIterator~waitForIframesDoneCallback} done - Done callback + */ + waitForIframes(ctx, done) { + let eachCalled = 0; + this.forEachIframe(ctx, () => true, (ifr) => { + eachCalled++; + this.waitForIframes(ifr.querySelector("html"), () => { + if (!--eachCalled) { + done(); + } + }); + }, (handled) => { + if (!handled) { + done(); + } + }); + } + /** + * Callback allowing to filter an iframe. Must return true when the element + * should remain, otherwise false + * @callback DOMIterator~forEachIframeFilterCallback + * @param {HTMLElement} iframe - The iframe DOM element + */ + /** + * Callback for each iframe content + * @callback DOMIterator~forEachIframeEachCallback + * @param {HTMLElement} content - The iframe document + */ + /** + * Callback if all iframes inside the context were handled + * @callback DOMIterator~forEachIframeEndCallback + * @param {number} handled - The number of handled iframes (those who + * wheren't filtered) + */ + /** + * Iterates over all iframes inside the specified context and calls the + * callbacks when they're ready. Filters iframes based on the instance + * exclusion selectors + * @param {HTMLElement} ctx - The context DOM element + * @param {DOMIterator~forEachIframeFilterCallback} filter - Filter callback + * @param {DOMIterator~forEachIframeEachCallback} each - Each callback + * @param {DOMIterator~forEachIframeEndCallback} [end] - End callback + * @access protected + */ + forEachIframe(ctx, filter, each, end = () => { + }) { + let ifr = ctx.querySelectorAll("iframe"), open = ifr.length, handled = 0; + ifr = Array.prototype.slice.call(ifr); + const checkEnd = () => { + if (--open <= 0) { + end(handled); + } + }; + if (!open) { + checkEnd(); + } + ifr.forEach((ifr2) => { + if (DOMIterator.matches(ifr2, this.exclude)) { + checkEnd(); + } else { + this.onIframeReady(ifr2, (con) => { + if (filter(ifr2)) { + handled++; + each(con); + } + checkEnd(); + }, checkEnd); + } + }); + } + /** + * Creates a NodeIterator on the specified context + * @see {@link https://developer.mozilla.org/en/docs/Web/API/NodeIterator} + * @param {HTMLElement} ctx - The context DOM element + * @param {DOMIterator~whatToShow} whatToShow + * @param {DOMIterator~filterCb} filter + * @return {NodeIterator} + * @access protected + */ + createIterator(ctx, whatToShow, filter) { + return document.createNodeIterator(ctx, whatToShow, filter, false); + } + /** + * Creates an instance of DOMIterator in an iframe + * @param {HTMLDocument} contents - Iframe document + * @return {DOMIterator} + * @access protected + */ + createInstanceOnIframe(contents) { + return new DOMIterator(contents.querySelector("html"), this.iframes); + } + /** + * Checks if an iframe occurs between two nodes, more specifically if an + * iframe occurs before the specified node and after the specified prevNode + * @param {HTMLElement} node - The node that should occur after the iframe + * @param {HTMLElement} prevNode - The node that should occur before the + * iframe + * @param {HTMLElement} ifr - The iframe to check against + * @return {boolean} + * @access protected + */ + compareNodeIframe(node, prevNode, ifr) { + const compCurr = node.compareDocumentPosition(ifr), prev = Node.DOCUMENT_POSITION_PRECEDING; + if (compCurr & prev) { + if (prevNode !== null) { + const compPrev = prevNode.compareDocumentPosition(ifr), after = Node.DOCUMENT_POSITION_FOLLOWING; + if (compPrev & after) { + return true; + } + } else { + return true; + } + } + return false; + } + /** + * @typedef {DOMIterator~getIteratorNodeReturn} + * @type {object.} + * @property {HTMLElement} prevNode - The previous node or null if there is + * no + * @property {HTMLElement} node - The current node + */ + /** + * Returns the previous and current node of the specified iterator + * @param {NodeIterator} itr - The iterator + * @return {DOMIterator~getIteratorNodeReturn} + * @access protected + */ + getIteratorNode(itr) { + const prevNode = itr.previousNode(); + let node; + if (prevNode === null) { + node = itr.nextNode(); + } else { + node = itr.nextNode() && itr.nextNode(); + } + return { + prevNode, + node + }; + } + /** + * An array containing objects. The object key "val" contains an iframe + * DOM element. The object key "handled" contains a boolean indicating if + * the iframe was handled already. + * It wouldn't be enough to save all open or all already handled iframes. + * The information of open iframes is necessary because they may occur after + * all other text nodes (and compareNodeIframe would never be true). The + * information of already handled iframes is necessary as otherwise they may + * be handled multiple times + * @typedef DOMIterator~checkIframeFilterIfr + * @type {object[]} + */ + /** + * Checks if an iframe wasn't handled already and if so, calls + * {@link DOMIterator#compareNodeIframe} to check if it should be handled. + * Information wheter an iframe was or wasn't handled is given within the + * ifr dictionary + * @param {HTMLElement} node - The node that should occur after the iframe + * @param {HTMLElement} prevNode - The node that should occur before the + * iframe + * @param {HTMLElement} currIfr - The iframe to check + * @param {DOMIterator~checkIframeFilterIfr} ifr - The iframe dictionary. + * Will be manipulated (by reference) + * @return {boolean} Returns true when it should be handled, otherwise false + * @access protected + */ + checkIframeFilter(node, prevNode, currIfr, ifr) { + let key = false, handled = false; + ifr.forEach((ifrDict, i) => { + if (ifrDict.val === currIfr) { + key = i; + handled = ifrDict.handled; + } + }); + if (this.compareNodeIframe(node, prevNode, currIfr)) { + if (key === false && !handled) { + ifr.push({ + val: currIfr, + handled: true + }); + } else if (key !== false && !handled) { + ifr[key].handled = true; + } + return true; + } + if (key === false) { + ifr.push({ + val: currIfr, + handled: false + }); + } + return false; + } + /** + * Creates an iterator on all open iframes in the specified array and calls + * the end callback when finished + * @param {DOMIterator~checkIframeFilterIfr} ifr + * @param {DOMIterator~whatToShow} whatToShow + * @param {DOMIterator~forEachNodeCallback} eCb - Each callback + * @param {DOMIterator~filterCb} fCb + * @access protected + */ + handleOpenIframes(ifr, whatToShow, eCb, fCb) { + ifr.forEach((ifrDict) => { + if (!ifrDict.handled) { + this.getIframeContents(ifrDict.val, (con) => { + this.createInstanceOnIframe(con).forEachNode( + whatToShow, + eCb, + fCb + ); + }); + } + }); + } + /** + * Iterates through all nodes in the specified context and handles iframe + * nodes at the correct position + * @param {DOMIterator~whatToShow} whatToShow + * @param {HTMLElement} ctx - The context + * @param {DOMIterator~forEachNodeCallback} eachCb - Each callback + * @param {DOMIterator~filterCb} filterCb - Filter callback + * @param {DOMIterator~forEachNodeEndCallback} doneCb - End callback + * @access protected + */ + iterateThroughNodes(whatToShow, ctx, eachCb, filterCb, doneCb) { + const itr = this.createIterator(ctx, whatToShow, filterCb); + let ifr = [], elements = [], node, prevNode, retrieveNodes = () => { + ({ + prevNode, + node + } = this.getIteratorNode(itr)); + return node; + }; + while (retrieveNodes()) { + if (this.iframes) { + this.forEachIframe(ctx, (currIfr) => { + return this.checkIframeFilter(node, prevNode, currIfr, ifr); + }, (con) => { + this.createInstanceOnIframe(con).forEachNode( + whatToShow, + (ifrNode) => elements.push(ifrNode), + filterCb + ); + }); + } + elements.push(node); + } + elements.forEach((node2) => { + eachCb(node2); + }); + if (this.iframes) { + this.handleOpenIframes(ifr, whatToShow, eachCb, filterCb); + } + doneCb(); + } + /** + * Callback for each node + * @callback DOMIterator~forEachNodeCallback + * @param {HTMLElement} node - The DOM text node element + */ + /** + * Callback if all contexts were handled + * @callback DOMIterator~forEachNodeEndCallback + */ + /** + * Iterates over all contexts and initializes + * {@link DOMIterator#iterateThroughNodes iterateThroughNodes} on them + * @param {DOMIterator~whatToShow} whatToShow + * @param {DOMIterator~forEachNodeCallback} each - Each callback + * @param {DOMIterator~filterCb} filter - Filter callback + * @param {DOMIterator~forEachNodeEndCallback} done - End callback + * @access public + */ + forEachNode(whatToShow, each, filter, done = () => { + }) { + const contexts = this.getContexts(); + let open = contexts.length; + if (!open) { + done(); + } + contexts.forEach((ctx) => { + const ready = () => { + this.iterateThroughNodes(whatToShow, ctx, each, filter, () => { + if (--open <= 0) { + done(); + } + }); + }; + if (this.iframes) { + this.waitForIframes(ctx, ready); + } else { + ready(); + } + }); + } + /** + * Callback to filter nodes. Can return e.g. NodeFilter.FILTER_ACCEPT or + * NodeFilter.FILTER_REJECT + * @see {@link http://tinyurl.com/zdczmm2} + * @callback DOMIterator~filterCb + * @param {HTMLElement} node - The node to filter + */ + /** + * @typedef DOMIterator~whatToShow + * @see {@link http://tinyurl.com/zfqqkx2} + * @type {number} + */ +} +let Mark$1 = class Mark { + // eslint-disable-line no-unused-vars + /** + * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM + * element, an array of DOM elements, a NodeList or a selector + */ + constructor(ctx) { + this.ctx = ctx; + this.ie = false; + const ua = window.navigator.userAgent; + if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) { + this.ie = true; + } + } + /** + * Options defined by the user. They will be initialized from one of the + * public methods. See {@link Mark#mark}, {@link Mark#markRegExp}, + * {@link Mark#markRanges} and {@link Mark#unmark} for option properties. + * @type {object} + * @param {object} [val] - An object that will be merged with defaults + * @access protected + */ + set opt(val) { + this._opt = Object.assign({}, { + "element": "", + "className": "", + "exclude": [], + "iframes": false, + "iframesTimeout": 5e3, + "separateWordSearch": true, + "diacritics": true, + "synonyms": {}, + "accuracy": "partially", + "acrossElements": false, + "caseSensitive": false, + "ignoreJoiners": false, + "ignoreGroups": 0, + "ignorePunctuation": [], + "wildcards": "disabled", + "each": () => { + }, + "noMatch": () => { + }, + "filter": () => true, + "done": () => { + }, + "debug": false, + "log": window.console + }, val); + } + get opt() { + return this._opt; + } + /** + * An instance of DOMIterator + * @type {DOMIterator} + * @access protected + */ + get iterator() { + return new DOMIterator( + this.ctx, + this.opt.iframes, + this.opt.exclude, + this.opt.iframesTimeout + ); + } + /** + * Logs a message if log is enabled + * @param {string} msg - The message to log + * @param {string} [level="debug"] - The log level, e.g. warn + * error, debug + * @access protected + */ + log(msg, level = "debug") { + const log = this.opt.log; + if (!this.opt.debug) { + return; + } + if (typeof log === "object" && typeof log[level] === "function") { + log[level](`mark.js: ${msg}`); + } + } + /** + * Escapes a string for usage within a regular expression + * @param {string} str - The string to escape + * @return {string} + * @access protected + */ + escapeStr(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + } + /** + * Creates a regular expression string to match the specified search + * term including synonyms, diacritics and accuracy if defined + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createRegExp(str) { + if (this.opt.wildcards !== "disabled") { + str = this.setupWildcardsRegExp(str); + } + str = this.escapeStr(str); + if (Object.keys(this.opt.synonyms).length) { + str = this.createSynonymsRegExp(str); + } + if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { + str = this.setupIgnoreJoinersRegExp(str); + } + if (this.opt.diacritics) { + str = this.createDiacriticsRegExp(str); + } + str = this.createMergedBlanksRegExp(str); + if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { + str = this.createJoinersRegExp(str); + } + if (this.opt.wildcards !== "disabled") { + str = this.createWildcardsRegExp(str); + } + str = this.createAccuracyRegExp(str); + return str; + } + /** + * Creates a regular expression string to match the defined synonyms + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createSynonymsRegExp(str) { + const syn = this.opt.synonyms, sens = this.opt.caseSensitive ? "" : "i", joinerPlaceholder = this.opt.ignoreJoiners || this.opt.ignorePunctuation.length ? "\0" : ""; + for (let index in syn) { + if (syn.hasOwnProperty(index)) { + const value = syn[index], k1 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(index) : this.escapeStr(index), k2 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(value) : this.escapeStr(value); + if (k1 !== "" && k2 !== "") { + str = str.replace( + new RegExp( + `(${this.escapeStr(k1)}|${this.escapeStr(k2)})`, + `gm${sens}` + ), + joinerPlaceholder + `(${this.processSynomyms(k1)}|${this.processSynomyms(k2)})` + joinerPlaceholder + ); + } + } + } + return str; + } + /** + * Setup synonyms to work with ignoreJoiners and or ignorePunctuation + * @param {string} str - synonym key or value to process + * @return {string} - processed synonym string + */ + processSynomyms(str) { + if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { + str = this.setupIgnoreJoinersRegExp(str); + } + return str; + } + /** + * Sets up the regular expression string to allow later insertion of + * wildcard regular expression matches + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + setupWildcardsRegExp(str) { + str = str.replace(/(?:\\)*\?/g, (val) => { + return val.charAt(0) === "\\" ? "?" : ""; + }); + return str.replace(/(?:\\)*\*/g, (val) => { + return val.charAt(0) === "\\" ? "*" : ""; + }); + } + /** + * Sets up the regular expression string to allow later insertion of + * wildcard regular expression matches + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createWildcardsRegExp(str) { + let spaces = this.opt.wildcards === "withSpaces"; + return str.replace(/\u0001/g, spaces ? "[\\S\\s]?" : "\\S?").replace(/\u0002/g, spaces ? "[\\S\\s]*?" : "\\S*"); + } + /** + * Sets up the regular expression string to allow later insertion of + * designated characters (soft hyphens & zero width characters) + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + setupIgnoreJoinersRegExp(str) { + return str.replace(/[^(|)\\]/g, (val, indx, original) => { + let nextChar = original.charAt(indx + 1); + if (/[(|)\\]/.test(nextChar) || nextChar === "") { + return val; + } else { + return val + "\0"; + } + }); + } + /** + * Creates a regular expression string to allow ignoring of designated + * characters (soft hyphens, zero width characters & punctuation) based on + * the specified option values of ignorePunctuation and + * ignoreJoiners + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createJoinersRegExp(str) { + let joiner = []; + const ignorePunctuation = this.opt.ignorePunctuation; + if (Array.isArray(ignorePunctuation) && ignorePunctuation.length) { + joiner.push(this.escapeStr(ignorePunctuation.join(""))); + } + if (this.opt.ignoreJoiners) { + joiner.push("\\u00ad\\u200b\\u200c\\u200d"); + } + return joiner.length ? str.split(/\u0000+/).join(`[${joiner.join("")}]*`) : str; + } + /** + * Creates a regular expression string to match diacritics + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createDiacriticsRegExp(str) { + const sens = this.opt.caseSensitive ? "" : "i", dct = this.opt.caseSensitive ? [ + "aàáảãạăằắẳẵặâầấẩẫậäåāą", + "AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ", + "cçćč", + "CÇĆČ", + "dđď", + "DĐĎ", + "eèéẻẽẹêềếểễệëěēę", + "EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ", + "iìíỉĩịîïī", + "IÌÍỈĨỊÎÏĪ", + "lł", + "LŁ", + "nñňń", + "NÑŇŃ", + "oòóỏõọôồốổỗộơởỡớờợöøō", + "OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ", + "rř", + "RŘ", + "sšśșş", + "SŠŚȘŞ", + "tťțţ", + "TŤȚŢ", + "uùúủũụưừứửữựûüůū", + "UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ", + "yýỳỷỹỵÿ", + "YÝỲỶỸỴŸ", + "zžżź", + "ZŽŻŹ" + ] : [ + "aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ", + "cçćčCÇĆČ", + "dđďDĐĎ", + "eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ", + "iìíỉĩịîïīIÌÍỈĨỊÎÏĪ", + "lłLŁ", + "nñňńNÑŇŃ", + "oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ", + "rřRŘ", + "sšśșşSŠŚȘŞ", + "tťțţTŤȚŢ", + "uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ", + "yýỳỷỹỵÿYÝỲỶỸỴŸ", + "zžżźZŽŻŹ" + ]; + let handled = []; + str.split("").forEach((ch) => { + dct.every((dct2) => { + if (dct2.indexOf(ch) !== -1) { + if (handled.indexOf(dct2) > -1) { + return false; + } + str = str.replace( + new RegExp(`[${dct2}]`, `gm${sens}`), + `[${dct2}]` + ); + handled.push(dct2); + } + return true; + }); + }); + return str; + } + /** + * Creates a regular expression string that merges whitespace characters + * including subsequent ones into a single pattern, one or multiple + * whitespaces + * @param {string} str - The search term to be used + * @return {string} + * @access protected + */ + createMergedBlanksRegExp(str) { + return str.replace(/[\s]+/gmi, "[\\s]+"); + } + /** + * Creates a regular expression string to match the specified string with + * the defined accuracy. As in the regular expression of "exactly" can be + * a group containing a blank at the beginning, all regular expressions will + * be created with two groups. The first group can be ignored (may contain + * the said blank), the second contains the actual match + * @param {string} str - The searm term to be used + * @return {str} + * @access protected + */ + createAccuracyRegExp(str) { + const chars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿"; + let acc = this.opt.accuracy, val = typeof acc === "string" ? acc : acc.value, ls = typeof acc === "string" ? [] : acc.limiters, lsJoin = ""; + ls.forEach((limiter) => { + lsJoin += `|${this.escapeStr(limiter)}`; + }); + switch (val) { + case "partially": + default: + return `()(${str})`; + case "complementary": + lsJoin = "\\s" + (lsJoin ? lsJoin : this.escapeStr(chars)); + return `()([^${lsJoin}]*${str}[^${lsJoin}]*)`; + case "exactly": + return `(^|\\s${lsJoin})(${str})(?=$|\\s${lsJoin})`; + } + } + /** + * @typedef Mark~separatedKeywords + * @type {object.} + * @property {array.} keywords - The list of keywords + * @property {number} length - The length + */ + /** + * Returns a list of keywords dependent on whether separate word search + * was defined. Also it filters empty keywords + * @param {array} sv - The array of keywords + * @return {Mark~separatedKeywords} + * @access protected + */ + getSeparatedKeywords(sv) { + let stack = []; + sv.forEach((kw) => { + if (!this.opt.separateWordSearch) { + if (kw.trim() && stack.indexOf(kw) === -1) { + stack.push(kw); + } + } else { + kw.split(" ").forEach((kwSplitted) => { + if (kwSplitted.trim() && stack.indexOf(kwSplitted) === -1) { + stack.push(kwSplitted); + } + }); + } + }); + return { + // sort because of https://git.io/v6USg + "keywords": stack.sort((a, b) => { + return b.length - a.length; + }), + "length": stack.length + }; + } + /** + * Check if a value is a number + * @param {number|string} value - the value to check; + * numeric strings allowed + * @return {boolean} + * @access protected + */ + isNumeric(value) { + return Number(parseFloat(value)) == value; + } + /** + * @typedef Mark~rangeObject + * @type {object} + * @property {number} start - The start position within the composite value + * @property {number} length - The length of the string to mark within the + * composite value. + */ + /** + * @typedef Mark~setOfRanges + * @type {object[]} + * @property {Mark~rangeObject} + */ + /** + * Returns a processed list of integer offset indexes that do not overlap + * each other, and remove any string values or additional elements + * @param {Mark~setOfRanges} array - unprocessed raw array + * @return {Mark~setOfRanges} - processed array with any invalid entries + * removed + * @throws Will throw an error if an array of objects is not passed + * @access protected + */ + checkRanges(array) { + if (!Array.isArray(array) || Object.prototype.toString.call(array[0]) !== "[object Object]") { + this.log("markRanges() will only accept an array of objects"); + this.opt.noMatch(array); + return []; + } + const stack = []; + let last2 = 0; + array.sort((a, b) => { + return a.start - b.start; + }).forEach((item) => { + let { start, end, valid } = this.callNoMatchOnInvalidRanges(item, last2); + if (valid) { + item.start = start; + item.length = end - start; + stack.push(item); + last2 = end; + } + }); + return stack; + } + /** + * @typedef Mark~validObject + * @type {object} + * @property {number} start - The start position within the composite value + * @property {number} end - The calculated end position within the composite + * value. + * @property {boolean} valid - boolean value indicating that the start and + * calculated end range is valid + */ + /** + * Initial validation of ranges for markRanges. Preliminary checks are done + * to ensure the start and length values exist and are not zero or non- + * numeric + * @param {Mark~rangeObject} range - the current range object + * @param {number} last - last index of range + * @return {Mark~validObject} + * @access protected + */ + callNoMatchOnInvalidRanges(range, last2) { + let start, end, valid = false; + if (range && typeof range.start !== "undefined") { + start = parseInt(range.start, 10); + end = start + parseInt(range.length, 10); + if (this.isNumeric(range.start) && this.isNumeric(range.length) && end - last2 > 0 && end - start > 0) { + valid = true; + } else { + this.log( + `Ignoring invalid or overlapping range: ${JSON.stringify(range)}` + ); + this.opt.noMatch(range); + } + } else { + this.log(`Ignoring invalid range: ${JSON.stringify(range)}`); + this.opt.noMatch(range); + } + return { + start, + end, + valid + }; + } + /** + * Check valid range for markRanges. Check ranges with access to the context + * string. Range values are double checked, lengths that extend the mark + * beyond the string length are limitied and ranges containing only + * whitespace are ignored + * @param {Mark~rangeObject} range - the current range object + * @param {number} originalLength - original length of the context string + * @param {string} string - current content string + * @return {Mark~validObject} + * @access protected + */ + checkWhitespaceRanges(range, originalLength, string) { + let end, valid = true, max = string.length, offset = originalLength - max, start = parseInt(range.start, 10) - offset; + start = start > max ? max : start; + end = start + parseInt(range.length, 10); + if (end > max) { + end = max; + this.log(`End range automatically set to the max value of ${max}`); + } + if (start < 0 || end - start < 0 || start > max || end > max) { + valid = false; + this.log(`Invalid range: ${JSON.stringify(range)}`); + this.opt.noMatch(range); + } else if (string.substring(start, end).replace(/\s+/g, "") === "") { + valid = false; + this.log("Skipping whitespace only range: " + JSON.stringify(range)); + this.opt.noMatch(range); + } + return { + start, + end, + valid + }; + } + /** + * @typedef Mark~getTextNodesDict + * @type {object.} + * @property {string} value - The composite value of all text nodes + * @property {object[]} nodes - An array of objects + * @property {number} nodes.start - The start position within the composite + * value + * @property {number} nodes.end - The end position within the composite + * value + * @property {HTMLElement} nodes.node - The DOM text node element + */ + /** + * Callback + * @callback Mark~getTextNodesCallback + * @param {Mark~getTextNodesDict} + */ + /** + * Calls the callback with an object containing all text nodes (including + * iframe text nodes) with start and end positions and the composite value + * of them (string) + * @param {Mark~getTextNodesCallback} cb - Callback + * @access protected + */ + getTextNodes(cb) { + let val = "", nodes = []; + this.iterator.forEachNode(NodeFilter.SHOW_TEXT, (node) => { + nodes.push({ + start: val.length, + end: (val += node.textContent).length, + node + }); + }, (node) => { + if (this.matchesExclude(node.parentNode)) { + return NodeFilter.FILTER_REJECT; + } else { + return NodeFilter.FILTER_ACCEPT; + } + }, () => { + cb({ + value: val, + nodes + }); + }); + } + /** + * Checks if an element matches any of the specified exclude selectors. Also + * it checks for elements in which no marks should be performed (e.g. + * script and style tags) and optionally already marked elements + * @param {HTMLElement} el - The element to check + * @return {boolean} + * @access protected + */ + matchesExclude(el) { + return DOMIterator.matches(el, this.opt.exclude.concat([ + // ignores the elements itself, not their childrens (selector *) + "script", + "style", + "title", + "head", + "html" + ])); + } + /** + * Wraps the instance element and class around matches that fit the start + * and end positions within the node + * @param {HTMLElement} node - The DOM text node + * @param {number} start - The position where to start wrapping + * @param {number} end - The position where to end wrapping + * @return {HTMLElement} Returns the splitted text node that will appear + * after the wrapped text node + * @access protected + */ + wrapRangeInTextNode(node, start, end) { + const hEl = !this.opt.element ? "mark" : this.opt.element, startNode = node.splitText(start), ret = startNode.splitText(end - start); + let repl = document.createElement(hEl); + repl.setAttribute("data-markjs", "true"); + if (this.opt.className) { + repl.setAttribute("class", this.opt.className); + } + repl.textContent = startNode.textContent; + startNode.parentNode.replaceChild(repl, startNode); + return ret; + } + /** + * @typedef Mark~wrapRangeInMappedTextNodeDict + * @type {object.} + * @property {string} value - The composite value of all text nodes + * @property {object[]} nodes - An array of objects + * @property {number} nodes.start - The start position within the composite + * value + * @property {number} nodes.end - The end position within the composite + * value + * @property {HTMLElement} nodes.node - The DOM text node element + */ + /** + * Each callback + * @callback Mark~wrapMatchesEachCallback + * @param {HTMLElement} node - The wrapped DOM element + * @param {number} lastIndex - The last matching position within the + * composite value of text nodes + */ + /** + * Filter callback + * @callback Mark~wrapMatchesFilterCallback + * @param {HTMLElement} node - The matching text node DOM element + */ + /** + * Determines matches by start and end positions using the text node + * dictionary even across text nodes and calls + * {@link Mark#wrapRangeInTextNode} to wrap them + * @param {Mark~wrapRangeInMappedTextNodeDict} dict - The dictionary + * @param {number} start - The start position of the match + * @param {number} end - The end position of the match + * @param {Mark~wrapMatchesFilterCallback} filterCb - Filter callback + * @param {Mark~wrapMatchesEachCallback} eachCb - Each callback + * @access protected + */ + wrapRangeInMappedTextNode(dict, start, end, filterCb, eachCb) { + dict.nodes.every((n, i) => { + const sibl = dict.nodes[i + 1]; + if (typeof sibl === "undefined" || sibl.start > start) { + if (!filterCb(n.node)) { + return false; + } + const s = start - n.start, e = (end > n.end ? n.end : end) - n.start, startStr = dict.value.substr(0, n.start), endStr = dict.value.substr(e + n.start); + n.node = this.wrapRangeInTextNode(n.node, s, e); + dict.value = startStr + endStr; + dict.nodes.forEach((k, j) => { + if (j >= i) { + if (dict.nodes[j].start > 0 && j !== i) { + dict.nodes[j].start -= e; + } + dict.nodes[j].end -= e; + } + }); + end -= e; + eachCb(n.node.previousSibling, n.start); + if (end > n.end) { + start = n.end; + } else { + return false; + } + } + return true; + }); + } + /** + * Filter callback before each wrapping + * @callback Mark~wrapMatchesFilterCallback + * @param {string} match - The matching string + * @param {HTMLElement} node - The text node where the match occurs + */ + /** + * Callback for each wrapped element + * @callback Mark~wrapMatchesEachCallback + * @param {HTMLElement} element - The marked DOM element + */ + /** + * Callback on end + * @callback Mark~wrapMatchesEndCallback + */ + /** + * Wraps the instance element and class around matches within single HTML + * elements in all contexts + * @param {RegExp} regex - The regular expression to be searched for + * @param {number} ignoreGroups - A number indicating the amount of RegExp + * matching groups to ignore + * @param {Mark~wrapMatchesFilterCallback} filterCb + * @param {Mark~wrapMatchesEachCallback} eachCb + * @param {Mark~wrapMatchesEndCallback} endCb + * @access protected + */ + wrapMatches(regex, ignoreGroups, filterCb, eachCb, endCb) { + const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; + this.getTextNodes((dict) => { + dict.nodes.forEach((node) => { + node = node.node; + let match; + while ((match = regex.exec(node.textContent)) !== null && match[matchIdx] !== "") { + if (!filterCb(match[matchIdx], node)) { + continue; + } + let pos = match.index; + if (matchIdx !== 0) { + for (let i = 1; i < matchIdx; i++) { + pos += match[i].length; + } + } + node = this.wrapRangeInTextNode( + node, + pos, + pos + match[matchIdx].length + ); + eachCb(node.previousSibling); + regex.lastIndex = 0; + } + }); + endCb(); + }); + } + /** + * Callback for each wrapped element + * @callback Mark~wrapMatchesAcrossElementsEachCallback + * @param {HTMLElement} element - The marked DOM element + */ + /** + * Filter callback before each wrapping + * @callback Mark~wrapMatchesAcrossElementsFilterCallback + * @param {string} match - The matching string + * @param {HTMLElement} node - The text node where the match occurs + */ + /** + * Callback on end + * @callback Mark~wrapMatchesAcrossElementsEndCallback + */ + /** + * Wraps the instance element and class around matches across all HTML + * elements in all contexts + * @param {RegExp} regex - The regular expression to be searched for + * @param {number} ignoreGroups - A number indicating the amount of RegExp + * matching groups to ignore + * @param {Mark~wrapMatchesAcrossElementsFilterCallback} filterCb + * @param {Mark~wrapMatchesAcrossElementsEachCallback} eachCb + * @param {Mark~wrapMatchesAcrossElementsEndCallback} endCb + * @access protected + */ + wrapMatchesAcrossElements(regex, ignoreGroups, filterCb, eachCb, endCb) { + const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; + this.getTextNodes((dict) => { + let match; + while ((match = regex.exec(dict.value)) !== null && match[matchIdx] !== "") { + let start = match.index; + if (matchIdx !== 0) { + for (let i = 1; i < matchIdx; i++) { + start += match[i].length; + } + } + const end = start + match[matchIdx].length; + this.wrapRangeInMappedTextNode(dict, start, end, (node) => { + return filterCb(match[matchIdx], node); + }, (node, lastIndex) => { + regex.lastIndex = lastIndex; + eachCb(node); + }); + } + endCb(); + }); + } + /** + * Callback for each wrapped element + * @callback Mark~wrapRangeFromIndexEachCallback + * @param {HTMLElement} element - The marked DOM element + * @param {Mark~rangeObject} range - the current range object; provided + * start and length values will be numeric integers modified from the + * provided original ranges. + */ + /** + * Filter callback before each wrapping + * @callback Mark~wrapRangeFromIndexFilterCallback + * @param {HTMLElement} node - The text node which includes the range + * @param {Mark~rangeObject} range - the current range object + * @param {string} match - string extracted from the matching range + * @param {number} counter - A counter indicating the number of all marks + */ + /** + * Callback on end + * @callback Mark~wrapRangeFromIndexEndCallback + */ + /** + * Wraps the indicated ranges across all HTML elements in all contexts + * @param {Mark~setOfRanges} ranges + * @param {Mark~wrapRangeFromIndexFilterCallback} filterCb + * @param {Mark~wrapRangeFromIndexEachCallback} eachCb + * @param {Mark~wrapRangeFromIndexEndCallback} endCb + * @access protected + */ + wrapRangeFromIndex(ranges, filterCb, eachCb, endCb) { + this.getTextNodes((dict) => { + const originalLength = dict.value.length; + ranges.forEach((range, counter) => { + let { start, end, valid } = this.checkWhitespaceRanges( + range, + originalLength, + dict.value + ); + if (valid) { + this.wrapRangeInMappedTextNode(dict, start, end, (node) => { + return filterCb( + node, + range, + dict.value.substring(start, end), + counter + ); + }, (node) => { + eachCb(node, range); + }); + } + }); + endCb(); + }); + } + /** + * Unwraps the specified DOM node with its content (text nodes or HTML) + * without destroying possibly present events (using innerHTML) and + * normalizes the parent at the end (merge splitted text nodes) + * @param {HTMLElement} node - The DOM node to unwrap + * @access protected + */ + unwrapMatches(node) { + const parent = node.parentNode; + let docFrag = document.createDocumentFragment(); + while (node.firstChild) { + docFrag.appendChild(node.removeChild(node.firstChild)); + } + parent.replaceChild(docFrag, node); + if (!this.ie) { + parent.normalize(); + } else { + this.normalizeTextNode(parent); + } + } + /** + * Normalizes text nodes. It's a workaround for the native normalize method + * that has a bug in IE (see attached link). Should only be used in IE + * browsers as it's slower than the native method. + * @see {@link http://tinyurl.com/z5asa8c} + * @param {HTMLElement} node - The DOM node to normalize + * @access protected + */ + normalizeTextNode(node) { + if (!node) { + return; + } + if (node.nodeType === 3) { + while (node.nextSibling && node.nextSibling.nodeType === 3) { + node.nodeValue += node.nextSibling.nodeValue; + node.parentNode.removeChild(node.nextSibling); + } + } else { + this.normalizeTextNode(node.firstChild); + } + this.normalizeTextNode(node.nextSibling); + } + /** + * Callback when finished + * @callback Mark~commonDoneCallback + * @param {number} totalMatches - The number of marked elements + */ + /** + * @typedef Mark~commonOptions + * @type {object.} + * @property {string} [element="mark"] - HTML element tag name + * @property {string} [className] - An optional class name + * @property {string[]} [exclude] - An array with exclusion selectors. + * Elements matching those selectors will be ignored + * @property {boolean} [iframes=false] - Whether to search inside iframes + * @property {Mark~commonDoneCallback} [done] + * @property {boolean} [debug=false] - Wheter to log messages + * @property {object} [log=window.console] - Where to log messages (only if + * debug is true) + */ + /** + * Callback for each marked element + * @callback Mark~markRegExpEachCallback + * @param {HTMLElement} element - The marked DOM element + */ + /** + * Callback if there were no matches + * @callback Mark~markRegExpNoMatchCallback + * @param {RegExp} regexp - The regular expression + */ + /** + * Callback to filter matches + * @callback Mark~markRegExpFilterCallback + * @param {HTMLElement} textNode - The text node which includes the match + * @param {string} match - The matching string for the RegExp + * @param {number} counter - A counter indicating the number of all marks + */ + /** + * These options also include the common options from + * {@link Mark~commonOptions} + * @typedef Mark~markRegExpOptions + * @type {object.} + * @property {Mark~markRegExpEachCallback} [each] + * @property {Mark~markRegExpNoMatchCallback} [noMatch] + * @property {Mark~markRegExpFilterCallback} [filter] + */ + /** + * Marks a custom regular expression + * @param {RegExp} regexp - The regular expression + * @param {Mark~markRegExpOptions} [opt] - Optional options object + * @access public + */ + markRegExp(regexp, opt) { + this.opt = opt; + this.log(`Searching with expression "${regexp}"`); + let totalMatches = 0, fn = "wrapMatches"; + const eachCb = (element) => { + totalMatches++; + this.opt.each(element); + }; + if (this.opt.acrossElements) { + fn = "wrapMatchesAcrossElements"; + } + this[fn](regexp, this.opt.ignoreGroups, (match, node) => { + return this.opt.filter(node, match, totalMatches); + }, eachCb, () => { + if (totalMatches === 0) { + this.opt.noMatch(regexp); + } + this.opt.done(totalMatches); + }); + } + /** + * Callback for each marked element + * @callback Mark~markEachCallback + * @param {HTMLElement} element - The marked DOM element + */ + /** + * Callback if there were no matches + * @callback Mark~markNoMatchCallback + * @param {RegExp} term - The search term that was not found + */ + /** + * Callback to filter matches + * @callback Mark~markFilterCallback + * @param {HTMLElement} textNode - The text node which includes the match + * @param {string} match - The matching term + * @param {number} totalCounter - A counter indicating the number of all + * marks + * @param {number} termCounter - A counter indicating the number of marks + * for the specific match + */ + /** + * @typedef Mark~markAccuracyObject + * @type {object.} + * @property {string} value - A accuracy string value + * @property {string[]} limiters - A custom array of limiters. For example + * ["-", ","] + */ + /** + * @typedef Mark~markAccuracySetting + * @type {string} + * @property {"partially"|"complementary"|"exactly"|Mark~markAccuracyObject} + * [accuracy="partially"] - Either one of the following string values: + *
      + *
    • partially: When searching for "lor" only "lor" inside + * "lorem" will be marked
    • + *
    • complementary: When searching for "lor" the whole word + * "lorem" will be marked
    • + *
    • exactly: When searching for "lor" only those exact words + * will be marked. In this example nothing inside "lorem". This value + * is equivalent to the previous option wordBoundary
    • + *
    + * Or an object containing two properties: + *
      + *
    • value: One of the above named string values
    • + *
    • limiters: A custom array of string limiters for accuracy + * "exactly" or "complementary"
    • + *
    + */ + /** + * @typedef Mark~markWildcardsSetting + * @type {string} + * @property {"disabled"|"enabled"|"withSpaces"} + * [wildcards="disabled"] - Set to any of the following string values: + *
      + *
    • disabled: Disable wildcard usage
    • + *
    • enabled: When searching for "lor?m", the "?" will match zero + * or one non-space character (e.g. "lorm", "loram", "lor3m", etc). When + * searching for "lor*m", the "*" will match zero or more non-space + * characters (e.g. "lorm", "loram", "lor123m", etc).
    • + *
    • withSpaces: When searching for "lor?m", the "?" will + * match zero or one space or non-space character (e.g. "lor m", "loram", + * etc). When searching for "lor*m", the "*" will match zero or more space + * or non-space characters (e.g. "lorm", "lore et dolor ipsum", "lor: m", + * etc).
    • + *
    + */ + /** + * @typedef Mark~markIgnorePunctuationSetting + * @type {string[]} + * @property {string} The strings in this setting will contain punctuation + * marks that will be ignored: + *
      + *
    • These punctuation marks can be between any characters, e.g. setting + * this option to ["'"] would match "Worlds", "World's" and + * "Wo'rlds"
    • + *
    • One or more apostrophes between the letters would still produce a + * match (e.g. "W'o''r'l'd's").
    • + *
    • A typical setting for this option could be as follows: + *
      ignorePunctuation: ":;.,-–—‒_(){}[]!'\"+=".split(""),
      This + * setting includes common punctuation as well as a minus, en-dash, + * em-dash and figure-dash + * ({@link https://en.wikipedia.org/wiki/Dash#Figure_dash ref}), as well + * as an underscore.
    • + *
    + */ + /** + * These options also include the common options from + * {@link Mark~commonOptions} + * @typedef Mark~markOptions + * @type {object.} + * @property {boolean} [separateWordSearch=true] - Whether to search for + * each word separated by a blank instead of the complete term + * @property {boolean} [diacritics=true] - If diacritic characters should be + * matched. ({@link https://en.wikipedia.org/wiki/Diacritic Diacritics}) + * @property {object} [synonyms] - An object with synonyms. The key will be + * a synonym for the value and the value for the key + * @property {Mark~markAccuracySetting} [accuracy] + * @property {Mark~markWildcardsSetting} [wildcards] + * @property {boolean} [acrossElements=false] - Whether to find matches + * across HTML elements. By default, only matches within single HTML + * elements will be found + * @property {boolean} [ignoreJoiners=false] - Whether to ignore word + * joiners inside of key words. These include soft-hyphens, zero-width + * space, zero-width non-joiners and zero-width joiners. + * @property {Mark~markIgnorePunctuationSetting} [ignorePunctuation] + * @property {Mark~markEachCallback} [each] + * @property {Mark~markNoMatchCallback} [noMatch] + * @property {Mark~markFilterCallback} [filter] + */ + /** + * Marks the specified search terms + * @param {string|string[]} [sv] - Search value, either a search string or + * an array containing multiple search strings + * @param {Mark~markOptions} [opt] - Optional options object + * @access public + */ + mark(sv, opt) { + this.opt = opt; + let totalMatches = 0, fn = "wrapMatches"; + const { + keywords: kwArr, + length: kwArrLen + } = this.getSeparatedKeywords(typeof sv === "string" ? [sv] : sv), sens = this.opt.caseSensitive ? "" : "i", handler = (kw) => { + let regex = new RegExp(this.createRegExp(kw), `gm${sens}`), matches2 = 0; + this.log(`Searching with expression "${regex}"`); + this[fn](regex, 1, (term, node) => { + return this.opt.filter(node, kw, totalMatches, matches2); + }, (element) => { + matches2++; + totalMatches++; + this.opt.each(element); + }, () => { + if (matches2 === 0) { + this.opt.noMatch(kw); + } + if (kwArr[kwArrLen - 1] === kw) { + this.opt.done(totalMatches); + } else { + handler(kwArr[kwArr.indexOf(kw) + 1]); + } + }); + }; + if (this.opt.acrossElements) { + fn = "wrapMatchesAcrossElements"; + } + if (kwArrLen === 0) { + this.opt.done(totalMatches); + } else { + handler(kwArr[0]); + } + } + /** + * Callback for each marked element + * @callback Mark~markRangesEachCallback + * @param {HTMLElement} element - The marked DOM element + * @param {array} range - array of range start and end points + */ + /** + * Callback if a processed range is invalid, out-of-bounds, overlaps another + * range, or only matches whitespace + * @callback Mark~markRangesNoMatchCallback + * @param {Mark~rangeObject} range - a range object + */ + /** + * Callback to filter matches + * @callback Mark~markRangesFilterCallback + * @param {HTMLElement} node - The text node which includes the range + * @param {array} range - array of range start and end points + * @param {string} match - string extracted from the matching range + * @param {number} counter - A counter indicating the number of all marks + */ + /** + * These options also include the common options from + * {@link Mark~commonOptions} + * @typedef Mark~markRangesOptions + * @type {object.} + * @property {Mark~markRangesEachCallback} [each] + * @property {Mark~markRangesNoMatchCallback} [noMatch] + * @property {Mark~markRangesFilterCallback} [filter] + */ + /** + * Marks an array of objects containing a start with an end or length of the + * string to mark + * @param {Mark~setOfRanges} rawRanges - The original (preprocessed) + * array of objects + * @param {Mark~markRangesOptions} [opt] - Optional options object + * @access public + */ + markRanges(rawRanges, opt) { + this.opt = opt; + let totalMatches = 0, ranges = this.checkRanges(rawRanges); + if (ranges && ranges.length) { + this.log( + "Starting to mark with the following ranges: " + JSON.stringify(ranges) + ); + this.wrapRangeFromIndex( + ranges, + (node, range, match, counter) => { + return this.opt.filter(node, range, match, counter); + }, + (element, range) => { + totalMatches++; + this.opt.each(element, range); + }, + () => { + this.opt.done(totalMatches); + } + ); + } else { + this.opt.done(totalMatches); + } + } + /** + * Removes all marked elements inside the context with their HTML and + * normalizes the parent at the end + * @param {Mark~commonOptions} [opt] - Optional options object + * @access public + */ + unmark(opt) { + this.opt = opt; + let sel = this.opt.element ? this.opt.element : "*"; + sel += "[data-markjs]"; + if (this.opt.className) { + sel += `.${this.opt.className}`; + } + this.log(`Removal selector "${sel}"`); + this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT, (node) => { + this.unwrapMatches(node); + }, (node) => { + const matchesSel = DOMIterator.matches(node, sel), matchesExclude = this.matchesExclude(node); + if (!matchesSel || matchesExclude) { + return NodeFilter.FILTER_REJECT; + } else { + return NodeFilter.FILTER_ACCEPT; + } + }, this.opt.done); + } +}; +function Mark2(ctx) { + const instance = new Mark$1(ctx); + this.mark = (sv, opt) => { + instance.mark(sv, opt); + return this; + }; + this.markRegExp = (sv, opt) => { + instance.markRegExp(sv, opt); + return this; + }; + this.markRanges = (sv, opt) => { + instance.markRanges(sv, opt); + return this; + }; + this.unmark = (opt) => { + instance.unmark(opt); + return this; + }; + return this; +} +const ENTRIES = "ENTRIES"; +const KEYS = "KEYS"; +const VALUES = "VALUES"; +const LEAF = ""; +class TreeIterator { + constructor(set, type) { + const node = set._tree; + const keys = Array.from(node.keys()); + this.set = set; + this._type = type; + this._path = keys.length > 0 ? [{ node, keys }] : []; + } + next() { + const value = this.dive(); + this.backtrack(); + return value; + } + dive() { + if (this._path.length === 0) { + return { done: true, value: void 0 }; + } + const { node, keys } = last$1(this._path); + if (last$1(keys) === LEAF) { + return { done: false, value: this.result() }; + } + const child = node.get(last$1(keys)); + this._path.push({ node: child, keys: Array.from(child.keys()) }); + return this.dive(); + } + backtrack() { + if (this._path.length === 0) { + return; + } + const keys = last$1(this._path).keys; + keys.pop(); + if (keys.length > 0) { + return; + } + this._path.pop(); + this.backtrack(); + } + key() { + return this.set._prefix + this._path.map(({ keys }) => last$1(keys)).filter((key) => key !== LEAF).join(""); + } + value() { + return last$1(this._path).node.get(LEAF); + } + result() { + switch (this._type) { + case VALUES: + return this.value(); + case KEYS: + return this.key(); + default: + return [this.key(), this.value()]; + } + } + [Symbol.iterator]() { + return this; + } +} +const last$1 = (array) => { + return array[array.length - 1]; +}; +const fuzzySearch = (node, query, maxDistance) => { + const results = /* @__PURE__ */ new Map(); + if (query === void 0) + return results; + const n = query.length + 1; + const m = n + maxDistance; + const matrix = new Uint8Array(m * n).fill(maxDistance + 1); + for (let j = 0; j < n; ++j) + matrix[j] = j; + for (let i = 1; i < m; ++i) + matrix[i * n] = i; + recurse(node, query, maxDistance, results, matrix, 1, n, ""); + return results; +}; +const recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => { + const offset = m * n; + key: for (const key of node.keys()) { + if (key === LEAF) { + const distance = matrix[offset - 1]; + if (distance <= maxDistance) { + results.set(prefix, [node.get(key), distance]); + } + } else { + let i = m; + for (let pos = 0; pos < key.length; ++pos, ++i) { + const char = key[pos]; + const thisRowOffset = n * i; + const prevRowOffset = thisRowOffset - n; + let minDistance = matrix[thisRowOffset]; + const jmin = Math.max(0, i - maxDistance - 1); + const jmax = Math.min(n - 1, i + maxDistance); + for (let j = jmin; j < jmax; ++j) { + const different = char !== query[j]; + const rpl = matrix[prevRowOffset + j] + +different; + const del = matrix[prevRowOffset + j + 1] + 1; + const ins = matrix[thisRowOffset + j] + 1; + const dist = matrix[thisRowOffset + j + 1] = Math.min(rpl, del, ins); + if (dist < minDistance) + minDistance = dist; + } + if (minDistance > maxDistance) { + continue key; + } + } + recurse(node.get(key), query, maxDistance, results, matrix, i, n, prefix + key); + } + } +}; +class SearchableMap { + /** + * The constructor is normally called without arguments, creating an empty + * map. In order to create a {@link SearchableMap} from an iterable or from an + * object, check {@link SearchableMap.from} and {@link + * SearchableMap.fromObject}. + * + * The constructor arguments are for internal use, when creating derived + * mutable views of a map at a prefix. + */ + constructor(tree = /* @__PURE__ */ new Map(), prefix = "") { + this._size = void 0; + this._tree = tree; + this._prefix = prefix; + } + /** + * Creates and returns a mutable view of this {@link SearchableMap}, + * containing only entries that share the given prefix. + * + * ### Usage: + * + * ```javascript + * let map = new SearchableMap() + * map.set("unicorn", 1) + * map.set("universe", 2) + * map.set("university", 3) + * map.set("unique", 4) + * map.set("hello", 5) + * + * let uni = map.atPrefix("uni") + * uni.get("unique") // => 4 + * uni.get("unicorn") // => 1 + * uni.get("hello") // => undefined + * + * let univer = map.atPrefix("univer") + * univer.get("unique") // => undefined + * univer.get("universe") // => 2 + * univer.get("university") // => 3 + * ``` + * + * @param prefix The prefix + * @return A {@link SearchableMap} representing a mutable view of the original + * Map at the given prefix + */ + atPrefix(prefix) { + if (!prefix.startsWith(this._prefix)) { + throw new Error("Mismatched prefix"); + } + const [node, path] = trackDown(this._tree, prefix.slice(this._prefix.length)); + if (node === void 0) { + const [parentNode, key] = last(path); + for (const k of parentNode.keys()) { + if (k !== LEAF && k.startsWith(key)) { + const node2 = /* @__PURE__ */ new Map(); + node2.set(k.slice(key.length), parentNode.get(k)); + return new SearchableMap(node2, prefix); + } + } + } + return new SearchableMap(node, prefix); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear + */ + clear() { + this._size = void 0; + this._tree.clear(); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete + * @param key Key to delete + */ + delete(key) { + this._size = void 0; + return remove(this._tree, key); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries + * @return An iterator iterating through `[key, value]` entries. + */ + entries() { + return new TreeIterator(this, ENTRIES); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach + * @param fn Iteration function + */ + forEach(fn) { + for (const [key, value] of this) { + fn(key, value, this); + } + } + /** + * Returns a Map of all the entries that have a key within the given edit + * distance from the search key. The keys of the returned Map are the matching + * keys, while the values are two-element arrays where the first element is + * the value associated to the key, and the second is the edit distance of the + * key to the search key. + * + * ### Usage: + * + * ```javascript + * let map = new SearchableMap() + * map.set('hello', 'world') + * map.set('hell', 'yeah') + * map.set('ciao', 'mondo') + * + * // Get all entries that match the key 'hallo' with a maximum edit distance of 2 + * map.fuzzyGet('hallo', 2) + * // => Map(2) { 'hello' => ['world', 1], 'hell' => ['yeah', 2] } + * + * // In the example, the "hello" key has value "world" and edit distance of 1 + * // (change "e" to "a"), the key "hell" has value "yeah" and edit distance of 2 + * // (change "e" to "a", delete "o") + * ``` + * + * @param key The search key + * @param maxEditDistance The maximum edit distance (Levenshtein) + * @return A Map of the matching keys to their value and edit distance + */ + fuzzyGet(key, maxEditDistance) { + return fuzzySearch(this._tree, key, maxEditDistance); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get + * @param key Key to get + * @return Value associated to the key, or `undefined` if the key is not + * found. + */ + get(key) { + const node = lookup(this._tree, key); + return node !== void 0 ? node.get(LEAF) : void 0; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has + * @param key Key + * @return True if the key is in the map, false otherwise + */ + has(key) { + const node = lookup(this._tree, key); + return node !== void 0 && node.has(LEAF); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys + * @return An `Iterable` iterating through keys + */ + keys() { + return new TreeIterator(this, KEYS); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set + * @param key Key to set + * @param value Value to associate to the key + * @return The {@link SearchableMap} itself, to allow chaining + */ + set(key, value) { + if (typeof key !== "string") { + throw new Error("key must be a string"); + } + this._size = void 0; + const node = createPath(this._tree, key); + node.set(LEAF, value); + return this; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size + */ + get size() { + if (this._size) { + return this._size; + } + this._size = 0; + const iter = this.entries(); + while (!iter.next().done) + this._size += 1; + return this._size; + } + /** + * Updates the value at the given key using the provided function. The function + * is called with the current value at the key, and its return value is used as + * the new value to be set. + * + * ### Example: + * + * ```javascript + * // Increment the current value by one + * searchableMap.update('somekey', (currentValue) => currentValue == null ? 0 : currentValue + 1) + * ``` + * + * If the value at the given key is or will be an object, it might not require + * re-assignment. In that case it is better to use `fetch()`, because it is + * faster. + * + * @param key The key to update + * @param fn The function used to compute the new value from the current one + * @return The {@link SearchableMap} itself, to allow chaining + */ + update(key, fn) { + if (typeof key !== "string") { + throw new Error("key must be a string"); + } + this._size = void 0; + const node = createPath(this._tree, key); + node.set(LEAF, fn(node.get(LEAF))); + return this; + } + /** + * Fetches the value of the given key. If the value does not exist, calls the + * given function to create a new value, which is inserted at the given key + * and subsequently returned. + * + * ### Example: + * + * ```javascript + * const map = searchableMap.fetch('somekey', () => new Map()) + * map.set('foo', 'bar') + * ``` + * + * @param key The key to update + * @param initial A function that creates a new value if the key does not exist + * @return The existing or new value at the given key + */ + fetch(key, initial) { + if (typeof key !== "string") { + throw new Error("key must be a string"); + } + this._size = void 0; + const node = createPath(this._tree, key); + let value = node.get(LEAF); + if (value === void 0) { + node.set(LEAF, value = initial()); + } + return value; + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values + * @return An `Iterable` iterating through values. + */ + values() { + return new TreeIterator(this, VALUES); + } + /** + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@iterator + */ + [Symbol.iterator]() { + return this.entries(); + } + /** + * Creates a {@link SearchableMap} from an `Iterable` of entries + * + * @param entries Entries to be inserted in the {@link SearchableMap} + * @return A new {@link SearchableMap} with the given entries + */ + static from(entries) { + const tree = new SearchableMap(); + for (const [key, value] of entries) { + tree.set(key, value); + } + return tree; + } + /** + * Creates a {@link SearchableMap} from the iterable properties of a JavaScript object + * + * @param object Object of entries for the {@link SearchableMap} + * @return A new {@link SearchableMap} with the given entries + */ + static fromObject(object) { + return SearchableMap.from(Object.entries(object)); + } +} +const trackDown = (tree, key, path = []) => { + if (key.length === 0 || tree == null) { + return [tree, path]; + } + for (const k of tree.keys()) { + if (k !== LEAF && key.startsWith(k)) { + path.push([tree, k]); + return trackDown(tree.get(k), key.slice(k.length), path); + } + } + path.push([tree, key]); + return trackDown(void 0, "", path); +}; +const lookup = (tree, key) => { + if (key.length === 0 || tree == null) { + return tree; + } + for (const k of tree.keys()) { + if (k !== LEAF && key.startsWith(k)) { + return lookup(tree.get(k), key.slice(k.length)); + } + } +}; +const createPath = (node, key) => { + const keyLength = key.length; + outer: for (let pos = 0; node && pos < keyLength; ) { + for (const k of node.keys()) { + if (k !== LEAF && key[pos] === k[0]) { + const len = Math.min(keyLength - pos, k.length); + let offset = 1; + while (offset < len && key[pos + offset] === k[offset]) + ++offset; + const child2 = node.get(k); + if (offset === k.length) { + node = child2; + } else { + const intermediate = /* @__PURE__ */ new Map(); + intermediate.set(k.slice(offset), child2); + node.set(key.slice(pos, pos + offset), intermediate); + node.delete(k); + node = intermediate; + } + pos += offset; + continue outer; + } + } + const child = /* @__PURE__ */ new Map(); + node.set(key.slice(pos), child); + return child; + } + return node; +}; +const remove = (tree, key) => { + const [node, path] = trackDown(tree, key); + if (node === void 0) { + return; + } + node.delete(LEAF); + if (node.size === 0) { + cleanup(path); + } else if (node.size === 1) { + const [key2, value] = node.entries().next().value; + merge(path, key2, value); + } +}; +const cleanup = (path) => { + if (path.length === 0) { + return; + } + const [node, key] = last(path); + node.delete(key); + if (node.size === 0) { + cleanup(path.slice(0, -1)); + } else if (node.size === 1) { + const [key2, value] = node.entries().next().value; + if (key2 !== LEAF) { + merge(path.slice(0, -1), key2, value); + } + } +}; +const merge = (path, key, value) => { + if (path.length === 0) { + return; + } + const [node, nodeKey] = last(path); + node.set(nodeKey + key, value); + node.delete(nodeKey); +}; +const last = (array) => { + return array[array.length - 1]; +}; +const OR = "or"; +const AND = "and"; +const AND_NOT = "and_not"; +class MiniSearch { + /** + * @param options Configuration options + * + * ### Examples: + * + * ```javascript + * // Create a search engine that indexes the 'title' and 'text' fields of your + * // documents: + * const miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * ``` + * + * ### ID Field: + * + * ```javascript + * // Your documents are assumed to include a unique 'id' field, but if you want + * // to use a different field for document identification, you can set the + * // 'idField' option: + * const miniSearch = new MiniSearch({ idField: 'key', fields: ['title', 'text'] }) + * ``` + * + * ### Options and defaults: + * + * ```javascript + * // The full set of options (here with their default value) is: + * const miniSearch = new MiniSearch({ + * // idField: field that uniquely identifies a document + * idField: 'id', + * + * // extractField: function used to get the value of a field in a document. + * // By default, it assumes the document is a flat object with field names as + * // property keys and field values as string property values, but custom logic + * // can be implemented by setting this option to a custom extractor function. + * extractField: (document, fieldName) => document[fieldName], + * + * // tokenize: function used to split fields into individual terms. By + * // default, it is also used to tokenize search queries, unless a specific + * // `tokenize` search option is supplied. When tokenizing an indexed field, + * // the field name is passed as the second argument. + * tokenize: (string, _fieldName) => string.split(SPACE_OR_PUNCTUATION), + * + * // processTerm: function used to process each tokenized term before + * // indexing. It can be used for stemming and normalization. Return a falsy + * // value in order to discard a term. By default, it is also used to process + * // search queries, unless a specific `processTerm` option is supplied as a + * // search option. When processing a term from a indexed field, the field + * // name is passed as the second argument. + * processTerm: (term, _fieldName) => term.toLowerCase(), + * + * // searchOptions: default search options, see the `search` method for + * // details + * searchOptions: undefined, + * + * // fields: document fields to be indexed. Mandatory, but not set by default + * fields: undefined + * + * // storeFields: document fields to be stored and returned as part of the + * // search results. + * storeFields: [] + * }) + * ``` + */ + constructor(options) { + if ((options === null || options === void 0 ? void 0 : options.fields) == null) { + throw new Error('MiniSearch: option "fields" must be provided'); + } + const autoVacuum = options.autoVacuum == null || options.autoVacuum === true ? defaultAutoVacuumOptions : options.autoVacuum; + this._options = { + ...defaultOptions, + ...options, + autoVacuum, + searchOptions: { ...defaultSearchOptions, ...options.searchOptions || {} }, + autoSuggestOptions: { ...defaultAutoSuggestOptions, ...options.autoSuggestOptions || {} } + }; + this._index = new SearchableMap(); + this._documentCount = 0; + this._documentIds = /* @__PURE__ */ new Map(); + this._idToShortId = /* @__PURE__ */ new Map(); + this._fieldIds = {}; + this._fieldLength = /* @__PURE__ */ new Map(); + this._avgFieldLength = []; + this._nextId = 0; + this._storedFields = /* @__PURE__ */ new Map(); + this._dirtCount = 0; + this._currentVacuum = null; + this._enqueuedVacuum = null; + this._enqueuedVacuumConditions = defaultVacuumConditions; + this.addFields(this._options.fields); + } + /** + * Adds a document to the index + * + * @param document The document to be indexed + */ + add(document2) { + const { extractField, stringifyField, tokenize, processTerm, fields, idField } = this._options; + const id = extractField(document2, idField); + if (id == null) { + throw new Error(`MiniSearch: document does not have ID field "${idField}"`); + } + if (this._idToShortId.has(id)) { + throw new Error(`MiniSearch: duplicate ID ${id}`); + } + const shortDocumentId = this.addDocumentId(id); + this.saveStoredFields(shortDocumentId, document2); + for (const field of fields) { + const fieldValue = extractField(document2, field); + if (fieldValue == null) + continue; + const tokens = tokenize(stringifyField(fieldValue, field), field); + const fieldId = this._fieldIds[field]; + const uniqueTerms = new Set(tokens).size; + this.addFieldLength(shortDocumentId, fieldId, this._documentCount - 1, uniqueTerms); + for (const term of tokens) { + const processedTerm = processTerm(term, field); + if (Array.isArray(processedTerm)) { + for (const t of processedTerm) { + this.addTerm(fieldId, shortDocumentId, t); + } + } else if (processedTerm) { + this.addTerm(fieldId, shortDocumentId, processedTerm); + } + } + } + } + /** + * Adds all the given documents to the index + * + * @param documents An array of documents to be indexed + */ + addAll(documents) { + for (const document2 of documents) + this.add(document2); + } + /** + * Adds all the given documents to the index asynchronously. + * + * Returns a promise that resolves (to `undefined`) when the indexing is done. + * This method is useful when index many documents, to avoid blocking the main + * thread. The indexing is performed asynchronously and in chunks. + * + * @param documents An array of documents to be indexed + * @param options Configuration options + * @return A promise resolving to `undefined` when the indexing is done + */ + addAllAsync(documents, options = {}) { + const { chunkSize = 10 } = options; + const acc = { chunk: [], promise: Promise.resolve() }; + const { chunk, promise } = documents.reduce(({ chunk: chunk2, promise: promise2 }, document2, i) => { + chunk2.push(document2); + if ((i + 1) % chunkSize === 0) { + return { + chunk: [], + promise: promise2.then(() => new Promise((resolve) => setTimeout(resolve, 0))).then(() => this.addAll(chunk2)) + }; + } else { + return { chunk: chunk2, promise: promise2 }; + } + }, acc); + return promise.then(() => this.addAll(chunk)); + } + /** + * Removes the given document from the index. + * + * The document to remove must NOT have changed between indexing and removal, + * otherwise the index will be corrupted. + * + * This method requires passing the full document to be removed (not just the + * ID), and immediately removes the document from the inverted index, allowing + * memory to be released. A convenient alternative is {@link + * MiniSearch#discard}, which needs only the document ID, and has the same + * visible effect, but delays cleaning up the index until the next vacuuming. + * + * @param document The document to be removed + */ + remove(document2) { + const { tokenize, processTerm, extractField, stringifyField, fields, idField } = this._options; + const id = extractField(document2, idField); + if (id == null) { + throw new Error(`MiniSearch: document does not have ID field "${idField}"`); + } + const shortId = this._idToShortId.get(id); + if (shortId == null) { + throw new Error(`MiniSearch: cannot remove document with ID ${id}: it is not in the index`); + } + for (const field of fields) { + const fieldValue = extractField(document2, field); + if (fieldValue == null) + continue; + const tokens = tokenize(stringifyField(fieldValue, field), field); + const fieldId = this._fieldIds[field]; + const uniqueTerms = new Set(tokens).size; + this.removeFieldLength(shortId, fieldId, this._documentCount, uniqueTerms); + for (const term of tokens) { + const processedTerm = processTerm(term, field); + if (Array.isArray(processedTerm)) { + for (const t of processedTerm) { + this.removeTerm(fieldId, shortId, t); + } + } else if (processedTerm) { + this.removeTerm(fieldId, shortId, processedTerm); + } + } + } + this._storedFields.delete(shortId); + this._documentIds.delete(shortId); + this._idToShortId.delete(id); + this._fieldLength.delete(shortId); + this._documentCount -= 1; + } + /** + * Removes all the given documents from the index. If called with no arguments, + * it removes _all_ documents from the index. + * + * @param documents The documents to be removed. If this argument is omitted, + * all documents are removed. Note that, for removing all documents, it is + * more efficient to call this method with no arguments than to pass all + * documents. + */ + removeAll(documents) { + if (documents) { + for (const document2 of documents) + this.remove(document2); + } else if (arguments.length > 0) { + throw new Error("Expected documents to be present. Omit the argument to remove all documents."); + } else { + this._index = new SearchableMap(); + this._documentCount = 0; + this._documentIds = /* @__PURE__ */ new Map(); + this._idToShortId = /* @__PURE__ */ new Map(); + this._fieldLength = /* @__PURE__ */ new Map(); + this._avgFieldLength = []; + this._storedFields = /* @__PURE__ */ new Map(); + this._nextId = 0; + } + } + /** + * Discards the document with the given ID, so it won't appear in search results + * + * It has the same visible effect of {@link MiniSearch.remove} (both cause the + * document to stop appearing in searches), but a different effect on the + * internal data structures: + * + * - {@link MiniSearch#remove} requires passing the full document to be + * removed as argument, and removes it from the inverted index immediately. + * + * - {@link MiniSearch#discard} instead only needs the document ID, and + * works by marking the current version of the document as discarded, so it + * is immediately ignored by searches. This is faster and more convenient + * than {@link MiniSearch#remove}, but the index is not immediately + * modified. To take care of that, vacuuming is performed after a certain + * number of documents are discarded, cleaning up the index and allowing + * memory to be released. + * + * After discarding a document, it is possible to re-add a new version, and + * only the new version will appear in searches. In other words, discarding + * and re-adding a document works exactly like removing and re-adding it. The + * {@link MiniSearch.replace} method can also be used to replace a document + * with a new version. + * + * #### Details about vacuuming + * + * Repetite calls to this method would leave obsolete document references in + * the index, invisible to searches. Two mechanisms take care of cleaning up: + * clean up during search, and vacuuming. + * + * - Upon search, whenever a discarded ID is found (and ignored for the + * results), references to the discarded document are removed from the + * inverted index entries for the search terms. This ensures that subsequent + * searches for the same terms do not need to skip these obsolete references + * again. + * + * - In addition, vacuuming is performed automatically by default (see the + * `autoVacuum` field in {@link Options}) after a certain number of + * documents are discarded. Vacuuming traverses all terms in the index, + * cleaning up all references to discarded documents. Vacuuming can also be + * triggered manually by calling {@link MiniSearch#vacuum}. + * + * @param id The ID of the document to be discarded + */ + discard(id) { + const shortId = this._idToShortId.get(id); + if (shortId == null) { + throw new Error(`MiniSearch: cannot discard document with ID ${id}: it is not in the index`); + } + this._idToShortId.delete(id); + this._documentIds.delete(shortId); + this._storedFields.delete(shortId); + (this._fieldLength.get(shortId) || []).forEach((fieldLength, fieldId) => { + this.removeFieldLength(shortId, fieldId, this._documentCount, fieldLength); + }); + this._fieldLength.delete(shortId); + this._documentCount -= 1; + this._dirtCount += 1; + this.maybeAutoVacuum(); + } + maybeAutoVacuum() { + if (this._options.autoVacuum === false) { + return; + } + const { minDirtFactor, minDirtCount, batchSize, batchWait } = this._options.autoVacuum; + this.conditionalVacuum({ batchSize, batchWait }, { minDirtCount, minDirtFactor }); + } + /** + * Discards the documents with the given IDs, so they won't appear in search + * results + * + * It is equivalent to calling {@link MiniSearch#discard} for all the given + * IDs, but with the optimization of triggering at most one automatic + * vacuuming at the end. + * + * Note: to remove all documents from the index, it is faster and more + * convenient to call {@link MiniSearch.removeAll} with no argument, instead + * of passing all IDs to this method. + */ + discardAll(ids) { + const autoVacuum = this._options.autoVacuum; + try { + this._options.autoVacuum = false; + for (const id of ids) { + this.discard(id); + } + } finally { + this._options.autoVacuum = autoVacuum; + } + this.maybeAutoVacuum(); + } + /** + * It replaces an existing document with the given updated version + * + * It works by discarding the current version and adding the updated one, so + * it is functionally equivalent to calling {@link MiniSearch#discard} + * followed by {@link MiniSearch#add}. The ID of the updated document should + * be the same as the original one. + * + * Since it uses {@link MiniSearch#discard} internally, this method relies on + * vacuuming to clean up obsolete document references from the index, allowing + * memory to be released (see {@link MiniSearch#discard}). + * + * @param updatedDocument The updated document to replace the old version + * with + */ + replace(updatedDocument) { + const { idField, extractField } = this._options; + const id = extractField(updatedDocument, idField); + this.discard(id); + this.add(updatedDocument); + } + /** + * Triggers a manual vacuuming, cleaning up references to discarded documents + * from the inverted index + * + * Vacuuming is only useful for applications that use the {@link + * MiniSearch#discard} or {@link MiniSearch#replace} methods. + * + * By default, vacuuming is performed automatically when needed (controlled by + * the `autoVacuum` field in {@link Options}), so there is usually no need to + * call this method, unless one wants to make sure to perform vacuuming at a + * specific moment. + * + * Vacuuming traverses all terms in the inverted index in batches, and cleans + * up references to discarded documents from the posting list, allowing memory + * to be released. + * + * The method takes an optional object as argument with the following keys: + * + * - `batchSize`: the size of each batch (1000 by default) + * + * - `batchWait`: the number of milliseconds to wait between batches (10 by + * default) + * + * On large indexes, vacuuming could have a non-negligible cost: batching + * avoids blocking the thread for long, diluting this cost so that it is not + * negatively affecting the application. Nonetheless, this method should only + * be called when necessary, and relying on automatic vacuuming is usually + * better. + * + * It returns a promise that resolves (to undefined) when the clean up is + * completed. If vacuuming is already ongoing at the time this method is + * called, a new one is enqueued immediately after the ongoing one, and a + * corresponding promise is returned. However, no more than one vacuuming is + * enqueued on top of the ongoing one, even if this method is called more + * times (enqueuing multiple ones would be useless). + * + * @param options Configuration options for the batch size and delay. See + * {@link VacuumOptions}. + */ + vacuum(options = {}) { + return this.conditionalVacuum(options); + } + conditionalVacuum(options, conditions) { + if (this._currentVacuum) { + this._enqueuedVacuumConditions = this._enqueuedVacuumConditions && conditions; + if (this._enqueuedVacuum != null) { + return this._enqueuedVacuum; + } + this._enqueuedVacuum = this._currentVacuum.then(() => { + const conditions2 = this._enqueuedVacuumConditions; + this._enqueuedVacuumConditions = defaultVacuumConditions; + return this.performVacuuming(options, conditions2); + }); + return this._enqueuedVacuum; + } + if (this.vacuumConditionsMet(conditions) === false) { + return Promise.resolve(); + } + this._currentVacuum = this.performVacuuming(options); + return this._currentVacuum; + } + async performVacuuming(options, conditions) { + const initialDirtCount = this._dirtCount; + if (this.vacuumConditionsMet(conditions)) { + const batchSize = options.batchSize || defaultVacuumOptions.batchSize; + const batchWait = options.batchWait || defaultVacuumOptions.batchWait; + let i = 1; + for (const [term, fieldsData] of this._index) { + for (const [fieldId, fieldIndex] of fieldsData) { + for (const [shortId] of fieldIndex) { + if (this._documentIds.has(shortId)) { + continue; + } + if (fieldIndex.size <= 1) { + fieldsData.delete(fieldId); + } else { + fieldIndex.delete(shortId); + } + } + } + if (this._index.get(term).size === 0) { + this._index.delete(term); + } + if (i % batchSize === 0) { + await new Promise((resolve) => setTimeout(resolve, batchWait)); + } + i += 1; + } + this._dirtCount -= initialDirtCount; + } + await null; + this._currentVacuum = this._enqueuedVacuum; + this._enqueuedVacuum = null; + } + vacuumConditionsMet(conditions) { + if (conditions == null) { + return true; + } + let { minDirtCount, minDirtFactor } = conditions; + minDirtCount = minDirtCount || defaultAutoVacuumOptions.minDirtCount; + minDirtFactor = minDirtFactor || defaultAutoVacuumOptions.minDirtFactor; + return this.dirtCount >= minDirtCount && this.dirtFactor >= minDirtFactor; + } + /** + * Is `true` if a vacuuming operation is ongoing, `false` otherwise + */ + get isVacuuming() { + return this._currentVacuum != null; + } + /** + * The number of documents discarded since the most recent vacuuming + */ + get dirtCount() { + return this._dirtCount; + } + /** + * A number between 0 and 1 giving an indication about the proportion of + * documents that are discarded, and can therefore be cleaned up by vacuuming. + * A value close to 0 means that the index is relatively clean, while a higher + * value means that the index is relatively dirty, and vacuuming could release + * memory. + */ + get dirtFactor() { + return this._dirtCount / (1 + this._documentCount + this._dirtCount); + } + /** + * Returns `true` if a document with the given ID is present in the index and + * available for search, `false` otherwise + * + * @param id The document ID + */ + has(id) { + return this._idToShortId.has(id); + } + /** + * Returns the stored fields (as configured in the `storeFields` constructor + * option) for the given document ID. Returns `undefined` if the document is + * not present in the index. + * + * @param id The document ID + */ + getStoredFields(id) { + const shortId = this._idToShortId.get(id); + if (shortId == null) { + return void 0; + } + return this._storedFields.get(shortId); + } + /** + * Search for documents matching the given search query. + * + * The result is a list of scored document IDs matching the query, sorted by + * descending score, and each including data about which terms were matched and + * in which fields. + * + * ### Basic usage: + * + * ```javascript + * // Search for "zen art motorcycle" with default options: terms have to match + * // exactly, and individual terms are joined with OR + * miniSearch.search('zen art motorcycle') + * // => [ { id: 2, score: 2.77258, match: { ... } }, { id: 4, score: 1.38629, match: { ... } } ] + * ``` + * + * ### Restrict search to specific fields: + * + * ```javascript + * // Search only in the 'title' field + * miniSearch.search('zen', { fields: ['title'] }) + * ``` + * + * ### Field boosting: + * + * ```javascript + * // Boost a field + * miniSearch.search('zen', { boost: { title: 2 } }) + * ``` + * + * ### Prefix search: + * + * ```javascript + * // Search for "moto" with prefix search (it will match documents + * // containing terms that start with "moto" or "neuro") + * miniSearch.search('moto neuro', { prefix: true }) + * ``` + * + * ### Fuzzy search: + * + * ```javascript + * // Search for "ismael" with fuzzy search (it will match documents containing + * // terms similar to "ismael", with a maximum edit distance of 0.2 term.length + * // (rounded to nearest integer) + * miniSearch.search('ismael', { fuzzy: 0.2 }) + * ``` + * + * ### Combining strategies: + * + * ```javascript + * // Mix of exact match, prefix search, and fuzzy search + * miniSearch.search('ismael mob', { + * prefix: true, + * fuzzy: 0.2 + * }) + * ``` + * + * ### Advanced prefix and fuzzy search: + * + * ```javascript + * // Perform fuzzy and prefix search depending on the search term. Here + * // performing prefix and fuzzy search only on terms longer than 3 characters + * miniSearch.search('ismael mob', { + * prefix: term => term.length > 3 + * fuzzy: term => term.length > 3 ? 0.2 : null + * }) + * ``` + * + * ### Combine with AND: + * + * ```javascript + * // Combine search terms with AND (to match only documents that contain both + * // "motorcycle" and "art") + * miniSearch.search('motorcycle art', { combineWith: 'AND' }) + * ``` + * + * ### Combine with AND_NOT: + * + * There is also an AND_NOT combinator, that finds documents that match the + * first term, but do not match any of the other terms. This combinator is + * rarely useful with simple queries, and is meant to be used with advanced + * query combinations (see later for more details). + * + * ### Filtering results: + * + * ```javascript + * // Filter only results in the 'fiction' category (assuming that 'category' + * // is a stored field) + * miniSearch.search('motorcycle art', { + * filter: (result) => result.category === 'fiction' + * }) + * ``` + * + * ### Wildcard query + * + * Searching for an empty string (assuming the default tokenizer) returns no + * results. Sometimes though, one needs to match all documents, like in a + * "wildcard" search. This is possible by passing the special value + * {@link MiniSearch.wildcard} as the query: + * + * ```javascript + * // Return search results for all documents + * miniSearch.search(MiniSearch.wildcard) + * ``` + * + * Note that search options such as `filter` and `boostDocument` are still + * applied, influencing which results are returned, and their order: + * + * ```javascript + * // Return search results for all documents in the 'fiction' category + * miniSearch.search(MiniSearch.wildcard, { + * filter: (result) => result.category === 'fiction' + * }) + * ``` + * + * ### Advanced combination of queries: + * + * It is possible to combine different subqueries with OR, AND, and AND_NOT, + * and even with different search options, by passing a query expression + * tree object as the first argument, instead of a string. + * + * ```javascript + * // Search for documents that contain "zen" and ("motorcycle" or "archery") + * miniSearch.search({ + * combineWith: 'AND', + * queries: [ + * 'zen', + * { + * combineWith: 'OR', + * queries: ['motorcycle', 'archery'] + * } + * ] + * }) + * + * // Search for documents that contain ("apple" or "pear") but not "juice" and + * // not "tree" + * miniSearch.search({ + * combineWith: 'AND_NOT', + * queries: [ + * { + * combineWith: 'OR', + * queries: ['apple', 'pear'] + * }, + * 'juice', + * 'tree' + * ] + * }) + * ``` + * + * Each node in the expression tree can be either a string, or an object that + * supports all {@link SearchOptions} fields, plus a `queries` array field for + * subqueries. + * + * Note that, while this can become complicated to do by hand for complex or + * deeply nested queries, it provides a formalized expression tree API for + * external libraries that implement a parser for custom query languages. + * + * @param query Search query + * @param searchOptions Search options. Each option, if not given, defaults to the corresponding value of `searchOptions` given to the constructor, or to the library default. + */ + search(query, searchOptions = {}) { + const { searchOptions: globalSearchOptions } = this._options; + const searchOptionsWithDefaults = { ...globalSearchOptions, ...searchOptions }; + const rawResults = this.executeQuery(query, searchOptions); + const results = []; + for (const [docId, { score, terms, match }] of rawResults) { + const quality = terms.length || 1; + const result = { + id: this._documentIds.get(docId), + score: score * quality, + terms: Object.keys(match), + queryTerms: terms, + match + }; + Object.assign(result, this._storedFields.get(docId)); + if (searchOptionsWithDefaults.filter == null || searchOptionsWithDefaults.filter(result)) { + results.push(result); + } + } + if (query === MiniSearch.wildcard && searchOptionsWithDefaults.boostDocument == null) { + return results; + } + results.sort(byScore); + return results; + } + /** + * Provide suggestions for the given search query + * + * The result is a list of suggested modified search queries, derived from the + * given search query, each with a relevance score, sorted by descending score. + * + * By default, it uses the same options used for search, except that by + * default it performs prefix search on the last term of the query, and + * combine terms with `'AND'` (requiring all query terms to match). Custom + * options can be passed as a second argument. Defaults can be changed upon + * calling the {@link MiniSearch} constructor, by passing a + * `autoSuggestOptions` option. + * + * ### Basic usage: + * + * ```javascript + * // Get suggestions for 'neuro': + * miniSearch.autoSuggest('neuro') + * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 0.46240 } ] + * ``` + * + * ### Multiple words: + * + * ```javascript + * // Get suggestions for 'zen ar': + * miniSearch.autoSuggest('zen ar') + * // => [ + * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 }, + * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 } + * // ] + * ``` + * + * ### Fuzzy suggestions: + * + * ```javascript + * // Correct spelling mistakes using fuzzy search: + * miniSearch.autoSuggest('neromancer', { fuzzy: 0.2 }) + * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 1.03998 } ] + * ``` + * + * ### Filtering: + * + * ```javascript + * // Get suggestions for 'zen ar', but only within the 'fiction' category + * // (assuming that 'category' is a stored field): + * miniSearch.autoSuggest('zen ar', { + * filter: (result) => result.category === 'fiction' + * }) + * // => [ + * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 }, + * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 } + * // ] + * ``` + * + * @param queryString Query string to be expanded into suggestions + * @param options Search options. The supported options and default values + * are the same as for the {@link MiniSearch#search} method, except that by + * default prefix search is performed on the last term in the query, and terms + * are combined with `'AND'`. + * @return A sorted array of suggestions sorted by relevance score. + */ + autoSuggest(queryString, options = {}) { + options = { ...this._options.autoSuggestOptions, ...options }; + const suggestions = /* @__PURE__ */ new Map(); + for (const { score, terms } of this.search(queryString, options)) { + const phrase = terms.join(" "); + const suggestion = suggestions.get(phrase); + if (suggestion != null) { + suggestion.score += score; + suggestion.count += 1; + } else { + suggestions.set(phrase, { score, terms, count: 1 }); + } + } + const results = []; + for (const [suggestion, { score, terms, count }] of suggestions) { + results.push({ suggestion, terms, score: score / count }); + } + results.sort(byScore); + return results; + } + /** + * Total number of documents available to search + */ + get documentCount() { + return this._documentCount; + } + /** + * Number of terms in the index + */ + get termCount() { + return this._index.size; + } + /** + * Deserializes a JSON index (serialized with `JSON.stringify(miniSearch)`) + * and instantiates a MiniSearch instance. It should be given the same options + * originally used when serializing the index. + * + * ### Usage: + * + * ```javascript + * // If the index was serialized with: + * let miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * miniSearch.addAll(documents) + * + * const json = JSON.stringify(miniSearch) + * // It can later be deserialized like this: + * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] }) + * ``` + * + * @param json JSON-serialized index + * @param options configuration options, same as the constructor + * @return An instance of MiniSearch deserialized from the given JSON. + */ + static loadJSON(json, options) { + if (options == null) { + throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index"); + } + return this.loadJS(JSON.parse(json), options); + } + /** + * Async equivalent of {@link MiniSearch.loadJSON} + * + * This function is an alternative to {@link MiniSearch.loadJSON} that returns + * a promise, and loads the index in batches, leaving pauses between them to avoid + * blocking the main thread. It tends to be slower than the synchronous + * version, but does not block the main thread, so it can be a better choice + * when deserializing very large indexes. + * + * @param json JSON-serialized index + * @param options configuration options, same as the constructor + * @return A Promise that will resolve to an instance of MiniSearch deserialized from the given JSON. + */ + static async loadJSONAsync(json, options) { + if (options == null) { + throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index"); + } + return this.loadJSAsync(JSON.parse(json), options); + } + /** + * Returns the default value of an option. It will throw an error if no option + * with the given name exists. + * + * @param optionName Name of the option + * @return The default value of the given option + * + * ### Usage: + * + * ```javascript + * // Get default tokenizer + * MiniSearch.getDefault('tokenize') + * + * // Get default term processor + * MiniSearch.getDefault('processTerm') + * + * // Unknown options will throw an error + * MiniSearch.getDefault('notExisting') + * // => throws 'MiniSearch: unknown option "notExisting"' + * ``` + */ + static getDefault(optionName) { + if (defaultOptions.hasOwnProperty(optionName)) { + return getOwnProperty(defaultOptions, optionName); + } else { + throw new Error(`MiniSearch: unknown option "${optionName}"`); + } + } + /** + * @ignore + */ + static loadJS(js, options) { + const { index, documentIds, fieldLength, storedFields, serializationVersion } = js; + const miniSearch = this.instantiateMiniSearch(js, options); + miniSearch._documentIds = objectToNumericMap(documentIds); + miniSearch._fieldLength = objectToNumericMap(fieldLength); + miniSearch._storedFields = objectToNumericMap(storedFields); + for (const [shortId, id] of miniSearch._documentIds) { + miniSearch._idToShortId.set(id, shortId); + } + for (const [term, data] of index) { + const dataMap = /* @__PURE__ */ new Map(); + for (const fieldId of Object.keys(data)) { + let indexEntry = data[fieldId]; + if (serializationVersion === 1) { + indexEntry = indexEntry.ds; + } + dataMap.set(parseInt(fieldId, 10), objectToNumericMap(indexEntry)); + } + miniSearch._index.set(term, dataMap); + } + return miniSearch; + } + /** + * @ignore + */ + static async loadJSAsync(js, options) { + const { index, documentIds, fieldLength, storedFields, serializationVersion } = js; + const miniSearch = this.instantiateMiniSearch(js, options); + miniSearch._documentIds = await objectToNumericMapAsync(documentIds); + miniSearch._fieldLength = await objectToNumericMapAsync(fieldLength); + miniSearch._storedFields = await objectToNumericMapAsync(storedFields); + for (const [shortId, id] of miniSearch._documentIds) { + miniSearch._idToShortId.set(id, shortId); + } + let count = 0; + for (const [term, data] of index) { + const dataMap = /* @__PURE__ */ new Map(); + for (const fieldId of Object.keys(data)) { + let indexEntry = data[fieldId]; + if (serializationVersion === 1) { + indexEntry = indexEntry.ds; + } + dataMap.set(parseInt(fieldId, 10), await objectToNumericMapAsync(indexEntry)); + } + if (++count % 1e3 === 0) + await wait(0); + miniSearch._index.set(term, dataMap); + } + return miniSearch; + } + /** + * @ignore + */ + static instantiateMiniSearch(js, options) { + const { documentCount, nextId, fieldIds, averageFieldLength, dirtCount, serializationVersion } = js; + if (serializationVersion !== 1 && serializationVersion !== 2) { + throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version"); + } + const miniSearch = new MiniSearch(options); + miniSearch._documentCount = documentCount; + miniSearch._nextId = nextId; + miniSearch._idToShortId = /* @__PURE__ */ new Map(); + miniSearch._fieldIds = fieldIds; + miniSearch._avgFieldLength = averageFieldLength; + miniSearch._dirtCount = dirtCount || 0; + miniSearch._index = new SearchableMap(); + return miniSearch; + } + /** + * @ignore + */ + executeQuery(query, searchOptions = {}) { + if (query === MiniSearch.wildcard) { + return this.executeWildcardQuery(searchOptions); + } + if (typeof query !== "string") { + const options2 = { ...searchOptions, ...query, queries: void 0 }; + const results2 = query.queries.map((subquery) => this.executeQuery(subquery, options2)); + return this.combineResults(results2, options2.combineWith); + } + const { tokenize, processTerm, searchOptions: globalSearchOptions } = this._options; + const options = { tokenize, processTerm, ...globalSearchOptions, ...searchOptions }; + const { tokenize: searchTokenize, processTerm: searchProcessTerm } = options; + const terms = searchTokenize(query).flatMap((term) => searchProcessTerm(term)).filter((term) => !!term); + const queries = terms.map(termToQuerySpec(options)); + const results = queries.map((query2) => this.executeQuerySpec(query2, options)); + return this.combineResults(results, options.combineWith); + } + /** + * @ignore + */ + executeQuerySpec(query, searchOptions) { + const options = { ...this._options.searchOptions, ...searchOptions }; + const boosts = (options.fields || this._options.fields).reduce((boosts2, field) => ({ ...boosts2, [field]: getOwnProperty(options.boost, field) || 1 }), {}); + const { boostDocument, weights, maxFuzzy, bm25: bm25params } = options; + const { fuzzy: fuzzyWeight, prefix: prefixWeight } = { ...defaultSearchOptions.weights, ...weights }; + const data = this._index.get(query.term); + const results = this.termResults(query.term, query.term, 1, query.termBoost, data, boosts, boostDocument, bm25params); + let prefixMatches; + let fuzzyMatches; + if (query.prefix) { + prefixMatches = this._index.atPrefix(query.term); + } + if (query.fuzzy) { + const fuzzy = query.fuzzy === true ? 0.2 : query.fuzzy; + const maxDistance = fuzzy < 1 ? Math.min(maxFuzzy, Math.round(query.term.length * fuzzy)) : fuzzy; + if (maxDistance) + fuzzyMatches = this._index.fuzzyGet(query.term, maxDistance); + } + if (prefixMatches) { + for (const [term, data2] of prefixMatches) { + const distance = term.length - query.term.length; + if (!distance) { + continue; + } + fuzzyMatches === null || fuzzyMatches === void 0 ? void 0 : fuzzyMatches.delete(term); + const weight = prefixWeight * term.length / (term.length + 0.3 * distance); + this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results); + } + } + if (fuzzyMatches) { + for (const term of fuzzyMatches.keys()) { + const [data2, distance] = fuzzyMatches.get(term); + if (!distance) { + continue; + } + const weight = fuzzyWeight * term.length / (term.length + distance); + this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results); + } + } + return results; + } + /** + * @ignore + */ + executeWildcardQuery(searchOptions) { + const results = /* @__PURE__ */ new Map(); + const options = { ...this._options.searchOptions, ...searchOptions }; + for (const [shortId, id] of this._documentIds) { + const score = options.boostDocument ? options.boostDocument(id, "", this._storedFields.get(shortId)) : 1; + results.set(shortId, { + score, + terms: [], + match: {} + }); + } + return results; + } + /** + * @ignore + */ + combineResults(results, combineWith = OR) { + if (results.length === 0) { + return /* @__PURE__ */ new Map(); + } + const operator = combineWith.toLowerCase(); + const combinator = combinators[operator]; + if (!combinator) { + throw new Error(`Invalid combination operator: ${combineWith}`); + } + return results.reduce(combinator) || /* @__PURE__ */ new Map(); + } + /** + * Allows serialization of the index to JSON, to possibly store it and later + * deserialize it with {@link MiniSearch.loadJSON}. + * + * Normally one does not directly call this method, but rather call the + * standard JavaScript `JSON.stringify()` passing the {@link MiniSearch} + * instance, and JavaScript will internally call this method. Upon + * deserialization, one must pass to {@link MiniSearch.loadJSON} the same + * options used to create the original instance that was serialized. + * + * ### Usage: + * + * ```javascript + * // Serialize the index: + * let miniSearch = new MiniSearch({ fields: ['title', 'text'] }) + * miniSearch.addAll(documents) + * const json = JSON.stringify(miniSearch) + * + * // Later, to deserialize it: + * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] }) + * ``` + * + * @return A plain-object serializable representation of the search index. + */ + toJSON() { + const index = []; + for (const [term, fieldIndex] of this._index) { + const data = {}; + for (const [fieldId, freqs] of fieldIndex) { + data[fieldId] = Object.fromEntries(freqs); + } + index.push([term, data]); + } + return { + documentCount: this._documentCount, + nextId: this._nextId, + documentIds: Object.fromEntries(this._documentIds), + fieldIds: this._fieldIds, + fieldLength: Object.fromEntries(this._fieldLength), + averageFieldLength: this._avgFieldLength, + storedFields: Object.fromEntries(this._storedFields), + dirtCount: this._dirtCount, + index, + serializationVersion: 2 + }; + } + /** + * @ignore + */ + termResults(sourceTerm, derivedTerm, termWeight, termBoost, fieldTermData, fieldBoosts, boostDocumentFn, bm25params, results = /* @__PURE__ */ new Map()) { + if (fieldTermData == null) + return results; + for (const field of Object.keys(fieldBoosts)) { + const fieldBoost = fieldBoosts[field]; + const fieldId = this._fieldIds[field]; + const fieldTermFreqs = fieldTermData.get(fieldId); + if (fieldTermFreqs == null) + continue; + let matchingFields = fieldTermFreqs.size; + const avgFieldLength = this._avgFieldLength[fieldId]; + for (const docId of fieldTermFreqs.keys()) { + if (!this._documentIds.has(docId)) { + this.removeTerm(fieldId, docId, derivedTerm); + matchingFields -= 1; + continue; + } + const docBoost = boostDocumentFn ? boostDocumentFn(this._documentIds.get(docId), derivedTerm, this._storedFields.get(docId)) : 1; + if (!docBoost) + continue; + const termFreq = fieldTermFreqs.get(docId); + const fieldLength = this._fieldLength.get(docId)[fieldId]; + const rawScore = calcBM25Score(termFreq, matchingFields, this._documentCount, fieldLength, avgFieldLength, bm25params); + const weightedScore = termWeight * termBoost * fieldBoost * docBoost * rawScore; + const result = results.get(docId); + if (result) { + result.score += weightedScore; + assignUniqueTerm(result.terms, sourceTerm); + const match = getOwnProperty(result.match, derivedTerm); + if (match) { + match.push(field); + } else { + result.match[derivedTerm] = [field]; + } + } else { + results.set(docId, { + score: weightedScore, + terms: [sourceTerm], + match: { [derivedTerm]: [field] } + }); + } + } + } + return results; + } + /** + * @ignore + */ + addTerm(fieldId, documentId, term) { + const indexData = this._index.fetch(term, createMap); + let fieldIndex = indexData.get(fieldId); + if (fieldIndex == null) { + fieldIndex = /* @__PURE__ */ new Map(); + fieldIndex.set(documentId, 1); + indexData.set(fieldId, fieldIndex); + } else { + const docs = fieldIndex.get(documentId); + fieldIndex.set(documentId, (docs || 0) + 1); + } + } + /** + * @ignore + */ + removeTerm(fieldId, documentId, term) { + if (!this._index.has(term)) { + this.warnDocumentChanged(documentId, fieldId, term); + return; + } + const indexData = this._index.fetch(term, createMap); + const fieldIndex = indexData.get(fieldId); + if (fieldIndex == null || fieldIndex.get(documentId) == null) { + this.warnDocumentChanged(documentId, fieldId, term); + } else if (fieldIndex.get(documentId) <= 1) { + if (fieldIndex.size <= 1) { + indexData.delete(fieldId); + } else { + fieldIndex.delete(documentId); + } + } else { + fieldIndex.set(documentId, fieldIndex.get(documentId) - 1); + } + if (this._index.get(term).size === 0) { + this._index.delete(term); + } + } + /** + * @ignore + */ + warnDocumentChanged(shortDocumentId, fieldId, term) { + for (const fieldName of Object.keys(this._fieldIds)) { + if (this._fieldIds[fieldName] === fieldId) { + this._options.logger("warn", `MiniSearch: document with ID ${this._documentIds.get(shortDocumentId)} has changed before removal: term "${term}" was not present in field "${fieldName}". Removing a document after it has changed can corrupt the index!`, "version_conflict"); + return; + } + } + } + /** + * @ignore + */ + addDocumentId(documentId) { + const shortDocumentId = this._nextId; + this._idToShortId.set(documentId, shortDocumentId); + this._documentIds.set(shortDocumentId, documentId); + this._documentCount += 1; + this._nextId += 1; + return shortDocumentId; + } + /** + * @ignore + */ + addFields(fields) { + for (let i = 0; i < fields.length; i++) { + this._fieldIds[fields[i]] = i; + } + } + /** + * @ignore + */ + addFieldLength(documentId, fieldId, count, length) { + let fieldLengths = this._fieldLength.get(documentId); + if (fieldLengths == null) + this._fieldLength.set(documentId, fieldLengths = []); + fieldLengths[fieldId] = length; + const averageFieldLength = this._avgFieldLength[fieldId] || 0; + const totalFieldLength = averageFieldLength * count + length; + this._avgFieldLength[fieldId] = totalFieldLength / (count + 1); + } + /** + * @ignore + */ + removeFieldLength(documentId, fieldId, count, length) { + if (count === 1) { + this._avgFieldLength[fieldId] = 0; + return; + } + const totalFieldLength = this._avgFieldLength[fieldId] * count - length; + this._avgFieldLength[fieldId] = totalFieldLength / (count - 1); + } + /** + * @ignore + */ + saveStoredFields(documentId, doc) { + const { storeFields, extractField } = this._options; + if (storeFields == null || storeFields.length === 0) { + return; + } + let documentFields = this._storedFields.get(documentId); + if (documentFields == null) + this._storedFields.set(documentId, documentFields = {}); + for (const fieldName of storeFields) { + const fieldValue = extractField(doc, fieldName); + if (fieldValue !== void 0) + documentFields[fieldName] = fieldValue; + } + } +} +MiniSearch.wildcard = Symbol("*"); +const getOwnProperty = (object, property) => Object.prototype.hasOwnProperty.call(object, property) ? object[property] : void 0; +const combinators = { + [OR]: (a, b) => { + for (const docId of b.keys()) { + const existing = a.get(docId); + if (existing == null) { + a.set(docId, b.get(docId)); + } else { + const { score, terms, match } = b.get(docId); + existing.score = existing.score + score; + existing.match = Object.assign(existing.match, match); + assignUniqueTerms(existing.terms, terms); + } + } + return a; + }, + [AND]: (a, b) => { + const combined = /* @__PURE__ */ new Map(); + for (const docId of b.keys()) { + const existing = a.get(docId); + if (existing == null) + continue; + const { score, terms, match } = b.get(docId); + assignUniqueTerms(existing.terms, terms); + combined.set(docId, { + score: existing.score + score, + terms: existing.terms, + match: Object.assign(existing.match, match) + }); + } + return combined; + }, + [AND_NOT]: (a, b) => { + for (const docId of b.keys()) + a.delete(docId); + return a; + } +}; +const defaultBM25params = { k: 1.2, b: 0.7, d: 0.5 }; +const calcBM25Score = (termFreq, matchingCount, totalCount, fieldLength, avgFieldLength, bm25params) => { + const { k, b, d } = bm25params; + const invDocFreq = Math.log(1 + (totalCount - matchingCount + 0.5) / (matchingCount + 0.5)); + return invDocFreq * (d + termFreq * (k + 1) / (termFreq + k * (1 - b + b * fieldLength / avgFieldLength))); +}; +const termToQuerySpec = (options) => (term, i, terms) => { + const fuzzy = typeof options.fuzzy === "function" ? options.fuzzy(term, i, terms) : options.fuzzy || false; + const prefix = typeof options.prefix === "function" ? options.prefix(term, i, terms) : options.prefix === true; + const termBoost = typeof options.boostTerm === "function" ? options.boostTerm(term, i, terms) : 1; + return { term, fuzzy, prefix, termBoost }; +}; +const defaultOptions = { + idField: "id", + extractField: (document2, fieldName) => document2[fieldName], + stringifyField: (fieldValue, fieldName) => fieldValue.toString(), + tokenize: (text) => text.split(SPACE_OR_PUNCTUATION), + processTerm: (term) => term.toLowerCase(), + fields: void 0, + searchOptions: void 0, + storeFields: [], + logger: (level, message) => { + if (typeof (console === null || console === void 0 ? void 0 : console[level]) === "function") + console[level](message); + }, + autoVacuum: true +}; +const defaultSearchOptions = { + combineWith: OR, + prefix: false, + fuzzy: false, + maxFuzzy: 6, + boost: {}, + weights: { fuzzy: 0.45, prefix: 0.375 }, + bm25: defaultBM25params +}; +const defaultAutoSuggestOptions = { + combineWith: AND, + prefix: (term, i, terms) => i === terms.length - 1 +}; +const defaultVacuumOptions = { batchSize: 1e3, batchWait: 10 }; +const defaultVacuumConditions = { minDirtFactor: 0.1, minDirtCount: 20 }; +const defaultAutoVacuumOptions = { ...defaultVacuumOptions, ...defaultVacuumConditions }; +const assignUniqueTerm = (target, term) => { + if (!target.includes(term)) + target.push(term); +}; +const assignUniqueTerms = (target, source) => { + for (const term of source) { + if (!target.includes(term)) + target.push(term); + } +}; +const byScore = ({ score: a }, { score: b }) => b - a; +const createMap = () => /* @__PURE__ */ new Map(); +const objectToNumericMap = (object) => { + const map = /* @__PURE__ */ new Map(); + for (const key of Object.keys(object)) { + map.set(parseInt(key, 10), object[key]); + } + return map; +}; +const objectToNumericMapAsync = async (object) => { + const map = /* @__PURE__ */ new Map(); + let count = 0; + for (const key of Object.keys(object)) { + map.set(parseInt(key, 10), object[key]); + if (++count % 1e3 === 0) { + await wait(0); + } + } + return map; +}; +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const SPACE_OR_PUNCTUATION = /[\n\r\p{Z}\p{P}]+/u; +class LRUCache { + constructor(max = 10) { + __publicField(this, "max"); + __publicField(this, "cache"); + this.max = max; + this.cache = /* @__PURE__ */ new Map(); + } + get(key) { + let item = this.cache.get(key); + if (item !== void 0) { + this.cache.delete(key); + this.cache.set(key, item); + } + return item; + } + set(key, val) { + if (this.cache.has(key)) + this.cache.delete(key); + else if (this.cache.size === this.max) + this.cache.delete(this.first()); + this.cache.set(key, val); + } + first() { + return this.cache.keys().next().value; + } + clear() { + this.cache.clear(); + } +} +const _hoisted_1 = ["aria-owns"]; +const _hoisted_2 = { class: "shell" }; +const _hoisted_3 = ["title"]; +const _hoisted_4 = { class: "search-actions before" }; +const _hoisted_5 = ["title"]; +const _hoisted_6 = ["aria-activedescendant", "aria-controls", "placeholder"]; +const _hoisted_7 = { class: "search-actions" }; +const _hoisted_8 = ["title"]; +const _hoisted_9 = ["disabled", "title"]; +const _hoisted_10 = ["id", "role", "aria-labelledby"]; +const _hoisted_11 = ["id", "aria-selected"]; +const _hoisted_12 = ["href", "aria-label", "onMouseenter", "onFocusin", "data-index"]; +const _hoisted_13 = { class: "titles" }; +const _hoisted_14 = ["innerHTML"]; +const _hoisted_15 = { class: "title main" }; +const _hoisted_16 = ["innerHTML"]; +const _hoisted_17 = { + key: 0, + class: "excerpt-wrapper" +}; +const _hoisted_18 = { + key: 0, + class: "excerpt", + inert: "" +}; +const _hoisted_19 = ["innerHTML"]; +const _hoisted_20 = { + key: 0, + class: "no-results" +}; +const _hoisted_21 = { class: "search-keyboard-shortcuts" }; +const _hoisted_22 = ["aria-label"]; +const _hoisted_23 = ["aria-label"]; +const _hoisted_24 = ["aria-label"]; +const _hoisted_25 = ["aria-label"]; +const _sfc_main = /* @__PURE__ */ defineComponent({ + __name: "VPLocalSearchBox", + emits: ["close"], + setup(__props, { emit: __emit }) { + var _a, _b; + const emit = __emit; + const el = shallowRef(); + const resultsEl = shallowRef(); + const searchIndexData = shallowRef(localSearchIndex); + const vitePressData = useData(); + const { activate } = useFocusTrap(el, { + immediate: true, + allowOutsideClick: true, + clickOutsideDeactivates: true, + escapeDeactivates: true + }); + const { localeIndex, theme } = vitePressData; + const searchIndex = computedAsync( + async () => { + var _a2, _b2, _c, _d, _e, _f, _g, _h, _i; + return markRaw( + MiniSearch.loadJSON( + (_c = await ((_b2 = (_a2 = searchIndexData.value)[localeIndex.value]) == null ? void 0 : _b2.call(_a2))) == null ? void 0 : _c.default, + { + fields: ["title", "titles", "text"], + storeFields: ["title", "titles"], + searchOptions: { + fuzzy: 0.2, + prefix: true, + boost: { title: 4, text: 2, titles: 1 }, + ...((_d = theme.value.search) == null ? void 0 : _d.provider) === "local" && ((_f = (_e = theme.value.search.options) == null ? void 0 : _e.miniSearch) == null ? void 0 : _f.searchOptions) + }, + ...((_g = theme.value.search) == null ? void 0 : _g.provider) === "local" && ((_i = (_h = theme.value.search.options) == null ? void 0 : _h.miniSearch) == null ? void 0 : _i.options) + } + ) + ); + } + ); + const disableQueryPersistence = computed(() => { + var _a2, _b2; + return ((_a2 = theme.value.search) == null ? void 0 : _a2.provider) === "local" && ((_b2 = theme.value.search.options) == null ? void 0 : _b2.disableQueryPersistence) === true; + }); + const filterText = disableQueryPersistence.value ? ref("") : useSessionStorage("vitepress:local-search-filter", ""); + const showDetailedList = useLocalStorage( + "vitepress:local-search-detailed-list", + ((_a = theme.value.search) == null ? void 0 : _a.provider) === "local" && ((_b = theme.value.search.options) == null ? void 0 : _b.detailedView) === true + ); + const disableDetailedView = computed(() => { + var _a2, _b2, _c; + return ((_a2 = theme.value.search) == null ? void 0 : _a2.provider) === "local" && (((_b2 = theme.value.search.options) == null ? void 0 : _b2.disableDetailedView) === true || ((_c = theme.value.search.options) == null ? void 0 : _c.detailedView) === false); + }); + const buttonText = computed(() => { + var _a2, _b2, _c, _d, _e, _f, _g; + const options = ((_a2 = theme.value.search) == null ? void 0 : _a2.options) ?? theme.value.algolia; + return ((_e = (_d = (_c = (_b2 = options == null ? void 0 : options.locales) == null ? void 0 : _b2[localeIndex.value]) == null ? void 0 : _c.translations) == null ? void 0 : _d.button) == null ? void 0 : _e.buttonText) || ((_g = (_f = options == null ? void 0 : options.translations) == null ? void 0 : _f.button) == null ? void 0 : _g.buttonText) || "Search"; + }); + watchEffect(() => { + if (disableDetailedView.value) { + showDetailedList.value = false; + } + }); + const results = shallowRef([]); + const enableNoResults = ref(false); + watch(filterText, () => { + enableNoResults.value = false; + }); + const mark = computedAsync(async () => { + if (!resultsEl.value) return; + return markRaw(new Mark2(resultsEl.value)); + }, null); + const cache = new LRUCache(16); + watchDebounced( + () => [searchIndex.value, filterText.value, showDetailedList.value], + async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { + var _a2, _b2, _c, _d; + if ((old == null ? void 0 : old[0]) !== index) { + cache.clear(); + } + let canceled = false; + onCleanup(() => { + canceled = true; + }); + if (!index) return; + results.value = index.search(filterTextValue).slice(0, 16); + enableNoResults.value = true; + const mods = showDetailedListValue ? await Promise.all(results.value.map((r) => fetchExcerpt(r.id))) : []; + if (canceled) return; + for (const { id, mod } of mods) { + const mapId = id.slice(0, id.indexOf("#")); + let map = cache.get(mapId); + if (map) continue; + map = /* @__PURE__ */ new Map(); + cache.set(mapId, map); + const comp = mod.default ?? mod; + if ((comp == null ? void 0 : comp.render) || (comp == null ? void 0 : comp.setup)) { + const app = createApp(comp); + app.config.warnHandler = () => { + }; + app.provide(dataSymbol, vitePressData); + Object.defineProperties(app.config.globalProperties, { + $frontmatter: { + get() { + return vitePressData.frontmatter.value; + } + }, + $params: { + get() { + return vitePressData.page.value.params; + } + } + }); + const div = document.createElement("div"); + app.mount(div); + const headings = div.querySelectorAll("h1, h2, h3, h4, h5, h6"); + headings.forEach((el2) => { + var _a3; + const href = (_a3 = el2.querySelector("a")) == null ? void 0 : _a3.getAttribute("href"); + const anchor = (href == null ? void 0 : href.startsWith("#")) && href.slice(1); + if (!anchor) return; + let html = ""; + while ((el2 = el2.nextElementSibling) && !/^h[1-6]$/i.test(el2.tagName)) + html += el2.outerHTML; + map.set(anchor, html); + }); + app.unmount(); + } + if (canceled) return; + } + const terms = /* @__PURE__ */ new Set(); + results.value = results.value.map((r) => { + const [id, anchor] = r.id.split("#"); + const map = cache.get(id); + const text = (map == null ? void 0 : map.get(anchor)) ?? ""; + for (const term in r.match) { + terms.add(term); + } + return { ...r, text }; + }); + await nextTick(); + if (canceled) return; + await new Promise((r) => { + var _a3; + (_a3 = mark.value) == null ? void 0 : _a3.unmark({ + done: () => { + var _a4; + (_a4 = mark.value) == null ? void 0 : _a4.markRegExp(formMarkRegex(terms), { done: r }); + } + }); + }); + const excerpts = ((_a2 = el.value) == null ? void 0 : _a2.querySelectorAll(".result .excerpt")) ?? []; + for (const excerpt of excerpts) { + (_b2 = excerpt.querySelector('mark[data-markjs="true"]')) == null ? void 0 : _b2.scrollIntoView({ block: "center" }); + } + (_d = (_c = resultsEl.value) == null ? void 0 : _c.firstElementChild) == null ? void 0 : _d.scrollIntoView({ block: "start" }); + }, + { debounce: 200, immediate: true } + ); + async function fetchExcerpt(id) { + const file = pathToFile(id.slice(0, id.indexOf("#"))); + try { + if (!file) throw new Error(`Cannot find file for id: ${id}`); + return { id, mod: await import( + /*@vite-ignore*/ + file + ) }; + } catch (e) { + console.error(e); + return { id, mod: {} }; + } + } + const searchInput = ref(); + const disableReset = computed(() => { + var _a2; + return ((_a2 = filterText.value) == null ? void 0 : _a2.length) <= 0; + }); + function focusSearchInput(select = true) { + var _a2, _b2; + (_a2 = searchInput.value) == null ? void 0 : _a2.focus(); + select && ((_b2 = searchInput.value) == null ? void 0 : _b2.select()); + } + onMounted(() => { + focusSearchInput(); + }); + function onSearchBarClick(event) { + if (event.pointerType === "mouse") { + focusSearchInput(); + } + } + const selectedIndex = ref(-1); + const disableMouseOver = ref(true); + watch(results, (r) => { + selectedIndex.value = r.length ? 0 : -1; + scrollToSelectedResult(); + }); + function scrollToSelectedResult() { + nextTick(() => { + const selectedEl = document.querySelector(".result.selected"); + selectedEl == null ? void 0 : selectedEl.scrollIntoView({ block: "nearest" }); + }); + } + onKeyStroke("ArrowUp", (event) => { + event.preventDefault(); + selectedIndex.value--; + if (selectedIndex.value < 0) { + selectedIndex.value = results.value.length - 1; + } + disableMouseOver.value = true; + scrollToSelectedResult(); + }); + onKeyStroke("ArrowDown", (event) => { + event.preventDefault(); + selectedIndex.value++; + if (selectedIndex.value >= results.value.length) { + selectedIndex.value = 0; + } + disableMouseOver.value = true; + scrollToSelectedResult(); + }); + const router = useRouter(); + onKeyStroke("Enter", (e) => { + if (e.isComposing) return; + if (e.target instanceof HTMLButtonElement && e.target.type !== "submit") + return; + const selectedPackage = results.value[selectedIndex.value]; + if (e.target instanceof HTMLInputElement && !selectedPackage) { + e.preventDefault(); + return; + } + if (selectedPackage) { + router.go(selectedPackage.id); + emit("close"); + } + }); + onKeyStroke("Escape", () => { + emit("close"); + }); + const defaultTranslations = { + modal: { + displayDetails: "Display detailed list", + resetButtonTitle: "Reset search", + backButtonTitle: "Close search", + noResultsText: "No results for", + footer: { + selectText: "to select", + selectKeyAriaLabel: "enter", + navigateText: "to navigate", + navigateUpKeyAriaLabel: "up arrow", + navigateDownKeyAriaLabel: "down arrow", + closeText: "to close", + closeKeyAriaLabel: "escape" + } + } + }; + const translate = createSearchTranslate(defaultTranslations); + onMounted(() => { + window.history.pushState(null, "", null); + }); + useEventListener("popstate", (event) => { + event.preventDefault(); + emit("close"); + }); + const isLocked = useScrollLock(inBrowser ? document.body : null); + onMounted(() => { + nextTick(() => { + isLocked.value = true; + nextTick().then(() => activate()); + }); + }); + onBeforeUnmount(() => { + isLocked.value = false; + }); + function resetSearch() { + filterText.value = ""; + nextTick().then(() => focusSearchInput(false)); + } + function formMarkRegex(terms) { + return new RegExp( + [...terms].sort((a, b) => b.length - a.length).map((term) => `(${escapeRegExp(term)})`).join("|"), + "gi" + ); + } + function onMouseMove(e) { + var _a2; + if (!disableMouseOver.value) return; + const el2 = (_a2 = e.target) == null ? void 0 : _a2.closest(".result"); + const index = Number.parseInt(el2 == null ? void 0 : el2.dataset.index); + if (index >= 0 && index !== selectedIndex.value) { + selectedIndex.value = index; + } + disableMouseOver.value = false; + } + return (_ctx, _cache) => { + var _a2, _b2, _c, _d, _e; + return openBlock(), createBlock(Teleport, { to: "body" }, [ + createBaseVNode("div", { + ref_key: "el", + ref: el, + role: "button", + "aria-owns": ((_a2 = results.value) == null ? void 0 : _a2.length) ? "localsearch-list" : void 0, + "aria-expanded": "true", + "aria-haspopup": "listbox", + "aria-labelledby": "localsearch-label", + class: "VPLocalSearchBox" + }, [ + createBaseVNode("div", { + class: "backdrop", + onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("close")) + }), + createBaseVNode("div", _hoisted_2, [ + createBaseVNode("form", { + class: "search-bar", + onPointerup: _cache[4] || (_cache[4] = ($event) => onSearchBarClick($event)), + onSubmit: _cache[5] || (_cache[5] = withModifiers(() => { + }, ["prevent"])) + }, [ + createBaseVNode("label", { + title: buttonText.value, + id: "localsearch-label", + for: "localsearch-input" + }, [..._cache[7] || (_cache[7] = [ + createBaseVNode("span", { + "aria-hidden": "true", + class: "vpi-search search-icon local-search-icon" + }, null, -1) + ])], 8, _hoisted_3), + createBaseVNode("div", _hoisted_4, [ + createBaseVNode("button", { + class: "back-button", + title: unref(translate)("modal.backButtonTitle"), + onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("close")) + }, [..._cache[8] || (_cache[8] = [ + createBaseVNode("span", { class: "vpi-arrow-left local-search-icon" }, null, -1) + ])], 8, _hoisted_5) + ]), + withDirectives(createBaseVNode("input", { + ref_key: "searchInput", + ref: searchInput, + "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => isRef(filterText) ? filterText.value = $event : null), + "aria-activedescendant": selectedIndex.value > -1 ? "localsearch-item-" + selectedIndex.value : void 0, + "aria-autocomplete": "both", + "aria-controls": ((_b2 = results.value) == null ? void 0 : _b2.length) ? "localsearch-list" : void 0, + "aria-labelledby": "localsearch-label", + autocapitalize: "off", + autocomplete: "off", + autocorrect: "off", + class: "search-input", + id: "localsearch-input", + enterkeyhint: "go", + maxlength: "64", + placeholder: buttonText.value, + spellcheck: "false", + type: "search" + }, null, 8, _hoisted_6), [ + [vModelText, unref(filterText)] + ]), + createBaseVNode("div", _hoisted_7, [ + !disableDetailedView.value ? (openBlock(), createElementBlock("button", { + key: 0, + class: normalizeClass(["toggle-layout-button", { "detailed-list": unref(showDetailedList) }]), + type: "button", + title: unref(translate)("modal.displayDetails"), + onClick: _cache[3] || (_cache[3] = ($event) => selectedIndex.value > -1 && (showDetailedList.value = !unref(showDetailedList))) + }, [..._cache[9] || (_cache[9] = [ + createBaseVNode("span", { class: "vpi-layout-list local-search-icon" }, null, -1) + ])], 10, _hoisted_8)) : createCommentVNode("", true), + createBaseVNode("button", { + class: "clear-button", + type: "reset", + disabled: disableReset.value, + title: unref(translate)("modal.resetButtonTitle"), + onClick: resetSearch + }, [..._cache[10] || (_cache[10] = [ + createBaseVNode("span", { class: "vpi-delete local-search-icon" }, null, -1) + ])], 8, _hoisted_9) + ]) + ], 32), + createBaseVNode("ul", { + ref_key: "resultsEl", + ref: resultsEl, + id: ((_c = results.value) == null ? void 0 : _c.length) ? "localsearch-list" : void 0, + role: ((_d = results.value) == null ? void 0 : _d.length) ? "listbox" : void 0, + "aria-labelledby": ((_e = results.value) == null ? void 0 : _e.length) ? "localsearch-label" : void 0, + class: "results", + onMousemove: onMouseMove + }, [ + (openBlock(true), createElementBlock(Fragment, null, renderList(results.value, (p, index) => { + return openBlock(), createElementBlock("li", { + key: p.id, + id: "localsearch-item-" + index, + "aria-selected": selectedIndex.value === index ? "true" : "false", + role: "option" + }, [ + createBaseVNode("a", { + href: p.id, + class: normalizeClass(["result", { + selected: selectedIndex.value === index + }]), + "aria-label": [...p.titles, p.title].join(" > "), + onMouseenter: ($event) => !disableMouseOver.value && (selectedIndex.value = index), + onFocusin: ($event) => selectedIndex.value = index, + onClick: _cache[6] || (_cache[6] = ($event) => _ctx.$emit("close")), + "data-index": index + }, [ + createBaseVNode("div", null, [ + createBaseVNode("div", _hoisted_13, [ + _cache[12] || (_cache[12] = createBaseVNode("span", { class: "title-icon" }, "#", -1)), + (openBlock(true), createElementBlock(Fragment, null, renderList(p.titles, (t, index2) => { + return openBlock(), createElementBlock("span", { + key: index2, + class: "title" + }, [ + createBaseVNode("span", { + class: "text", + innerHTML: t + }, null, 8, _hoisted_14), + _cache[11] || (_cache[11] = createBaseVNode("span", { class: "vpi-chevron-right local-search-icon" }, null, -1)) + ]); + }), 128)), + createBaseVNode("span", _hoisted_15, [ + createBaseVNode("span", { + class: "text", + innerHTML: p.title + }, null, 8, _hoisted_16) + ]) + ]), + unref(showDetailedList) ? (openBlock(), createElementBlock("div", _hoisted_17, [ + p.text ? (openBlock(), createElementBlock("div", _hoisted_18, [ + createBaseVNode("div", { + class: "vp-doc", + innerHTML: p.text + }, null, 8, _hoisted_19) + ])) : createCommentVNode("", true), + _cache[13] || (_cache[13] = createBaseVNode("div", { class: "excerpt-gradient-bottom" }, null, -1)), + _cache[14] || (_cache[14] = createBaseVNode("div", { class: "excerpt-gradient-top" }, null, -1)) + ])) : createCommentVNode("", true) + ]) + ], 42, _hoisted_12) + ], 8, _hoisted_11); + }), 128)), + unref(filterText) && !results.value.length && enableNoResults.value ? (openBlock(), createElementBlock("li", _hoisted_20, [ + createTextVNode(toDisplayString(unref(translate)("modal.noResultsText")) + ' "', 1), + createBaseVNode("strong", null, toDisplayString(unref(filterText)), 1), + _cache[15] || (_cache[15] = createTextVNode('" ', -1)) + ])) : createCommentVNode("", true) + ], 40, _hoisted_10), + createBaseVNode("div", _hoisted_21, [ + createBaseVNode("span", null, [ + createBaseVNode("kbd", { + "aria-label": unref(translate)("modal.footer.navigateUpKeyAriaLabel") + }, [..._cache[16] || (_cache[16] = [ + createBaseVNode("span", { class: "vpi-arrow-up navigate-icon" }, null, -1) + ])], 8, _hoisted_22), + createBaseVNode("kbd", { + "aria-label": unref(translate)("modal.footer.navigateDownKeyAriaLabel") + }, [..._cache[17] || (_cache[17] = [ + createBaseVNode("span", { class: "vpi-arrow-down navigate-icon" }, null, -1) + ])], 8, _hoisted_23), + createTextVNode(" " + toDisplayString(unref(translate)("modal.footer.navigateText")), 1) + ]), + createBaseVNode("span", null, [ + createBaseVNode("kbd", { + "aria-label": unref(translate)("modal.footer.selectKeyAriaLabel") + }, [..._cache[18] || (_cache[18] = [ + createBaseVNode("span", { class: "vpi-corner-down-left navigate-icon" }, null, -1) + ])], 8, _hoisted_24), + createTextVNode(" " + toDisplayString(unref(translate)("modal.footer.selectText")), 1) + ]), + createBaseVNode("span", null, [ + createBaseVNode("kbd", { + "aria-label": unref(translate)("modal.footer.closeKeyAriaLabel") + }, "esc", 8, _hoisted_25), + createTextVNode(" " + toDisplayString(unref(translate)("modal.footer.closeText")), 1) + ]) + ]) + ]) + ], 8, _hoisted_1) + ]); + }; + } +}); +const VPLocalSearchBox = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-ce626c7c"]]); +export { + VPLocalSearchBox as default +}; diff --git a/docs/.vitepress/dist/assets/chunks/framework.BcMzFyCJ.js b/docs/.vitepress/dist/assets/chunks/framework.BcMzFyCJ.js new file mode 100644 index 0000000..1afe658 --- /dev/null +++ b/docs/.vitepress/dist/assets/chunks/framework.BcMzFyCJ.js @@ -0,0 +1,10587 @@ +/** +* @vue/shared v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +// @__NO_SIDE_EFFECTS__ +function makeMap(str) { + const map = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map[key] = 1; + return (val) => val in map; +} +const EMPTY_OBJ = {}; +const EMPTY_ARR = []; +const NOOP = () => { +}; +const NO = () => false; +const isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter +(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); +const isModelListener = (key) => key.startsWith("onUpdate:"); +const extend = Object.assign; +const remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +const hasOwnProperty$1 = Object.prototype.hasOwnProperty; +const hasOwn = (val, key) => hasOwnProperty$1.call(val, key); +const isArray = Array.isArray; +const isMap = (val) => toTypeString(val) === "[object Map]"; +const isSet = (val) => toTypeString(val) === "[object Set]"; +const isDate = (val) => toTypeString(val) === "[object Date]"; +const isFunction = (val) => typeof val === "function"; +const isString = (val) => typeof val === "string"; +const isSymbol = (val) => typeof val === "symbol"; +const isObject$1 = (val) => val !== null && typeof val === "object"; +const isPromise = (val) => { + return (isObject$1(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); +}; +const objectToString = Object.prototype.toString; +const toTypeString = (value) => objectToString.call(value); +const toRawType = (value) => { + return toTypeString(value).slice(8, -1); +}; +const isPlainObject = (val) => toTypeString(val) === "[object Object]"; +const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; +const isReservedProp = /* @__PURE__ */ makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" +); +const cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return (str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; +}; +const camelizeRE = /-\w/g; +const camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase()); + } +); +const hyphenateRE = /\B([A-Z])/g; +const hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() +); +const capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}); +const toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } +); +const hasChanged = (value, oldValue) => !Object.is(value, oldValue); +const invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } +}; +const def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); +}; +const looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +const toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; +}; +let _globalThis; +const getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); +}; +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject$1(value)) { + return value; + } +} +const listDelimiterRE = /;(?![^(]*\))/g; +const propertyDelimiterRE = /:([^]+)/; +const styleCommentRE = /\/\*[^]*?\*\//g; +function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject$1(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); +} +const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +const isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs); +const isBooleanAttr = /* @__PURE__ */ makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` +); +function includeBooleanAttr(value) { + return !!value || value === ""; +} +const isKnownHtmlAttr = /* @__PURE__ */ makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` +); +const isKnownSvgAttr = /* @__PURE__ */ makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` +); +function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; +} +const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; +function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => `\\${s}` + ); +} +function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject$1(a); + bValidType = isObject$1(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +const isRef$1 = (val) => { + return !!(val && val["__v_isRef"] === true); +}; +const toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject$1(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef$1(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); +}; +const replacer = (_key, val) => { + if (isRef$1(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject$1(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; +}; +const stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); +}; +function normalizeCssVarValue(value) { + if (value == null) { + return "initial"; + } + if (typeof value === "string") { + return value === "" ? " " : value; + } + return String(value); +} +/** +* @vue/reactivity v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +let activeEffectScope; +class EffectScope { + // TODO isolatedDeclarations "__v_skip" + constructor(detached = false) { + this.detached = detached; + this._active = true; + this._on = 0; + this.effects = []; + this.cleanups = []; + this._isPaused = false; + this._warnOnRun = true; + this.__v_skip = true; + if (!detached && activeEffectScope) { + if (activeEffectScope.active) { + this.parent = activeEffectScope; + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } else { + this._active = false; + this._warnOnRun = false; + } + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + if (++this._on === 1) { + this.prevScope = activeEffectScope; + activeEffectScope = this; + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + if (this._on > 0 && --this._on === 0) { + if (activeEffectScope === this) { + activeEffectScope = this.prevScope; + } else { + let current = activeEffectScope; + while (current) { + if (current.prevScope === this) { + current.prevScope = this.prevScope; + break; + } + current = current.prevScope; + } + } + this.prevScope = void 0; + } + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } +} +function getCurrentScope() { + return activeEffectScope; +} +function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } +} +let activeSub; +const pausedQueueEffects = /* @__PURE__ */ new WeakSet(); +class ReactiveEffect { + constructor(fn) { + this.fn = fn; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 1 | 4; + this.next = void 0; + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope) { + if (activeEffectScope.active) { + activeEffectScope.effects.push(this); + } else { + this.flags &= -2; + } + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= -65; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= -3; + } + } + stop() { + if (this.flags & 1) { + for (let link2 = this.deps; link2; link2 = link2.nextDep) { + removeSub(link2); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= -2; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } +} +let batchDepth = 0; +let batchedSub; +let batchedComputed; +function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; +} +function startBatch() { + batchDepth++; +} +function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; +} +function prepareDeps(sub) { + for (let link2 = sub.deps; link2; link2 = link2.nextDep) { + link2.version = -1; + link2.prevActiveLink = link2.dep.activeLink; + link2.dep.activeLink = link2; + } +} +function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link2 = tail; + while (link2) { + const prev = link2.prevDep; + if (link2.version === -1) { + if (link2 === tail) tail = prev; + removeSub(link2); + removeDep(link2); + } else { + head = link2; + } + link2.dep.activeLink = link2.prevActiveLink; + link2.prevActiveLink = void 0; + link2 = prev; + } + sub.deps = head; + sub.depsTail = tail; +} +function isDirty(sub) { + for (let link2 = sub.deps; link2; link2 = link2.nextDep) { + if (link2.dep.version !== link2.version || link2.dep.computed && (refreshComputed(link2.dep.computed) || link2.dep.version !== link2.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; +} +function refreshComputed(computed2) { + if (computed2.flags & 4 && !(computed2.flags & 16)) { + return; + } + computed2.flags &= -17; + if (computed2.globalVersion === globalVersion) { + return; + } + computed2.globalVersion = globalVersion; + if (!computed2.isSSR && computed2.flags & 128 && (!computed2.deps && !computed2._dirty || !isDirty(computed2))) { + return; + } + computed2.flags |= 2; + const dep = computed2.dep; + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed2; + shouldTrack = true; + try { + prepareDeps(computed2); + const value = computed2.fn(computed2._value); + if (dep.version === 0 || hasChanged(value, computed2._value)) { + computed2.flags |= 128; + computed2._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed2); + computed2.flags &= -3; + } +} +function removeSub(link2, soft = false) { + const { dep, prevSub, nextSub } = link2; + if (prevSub) { + prevSub.nextSub = nextSub; + link2.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link2.nextSub = void 0; + } + if (dep.subs === link2) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= -5; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } +} +function removeDep(link2) { + const { prevDep, nextDep } = link2; + if (prevDep) { + prevDep.nextDep = nextDep; + link2.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link2.nextDep = void 0; + } +} +let shouldTrack = true; +const trackStack = []; +function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; +} +function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; +} +function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } +} +let globalVersion = 0; +class Link { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } +} +class Dep { + // TODO isolatedDeclarations "__v_skip" + constructor(computed2) { + this.computed = computed2; + this.version = 0; + this.activeLink = void 0; + this.subs = void 0; + this.map = void 0; + this.key = void 0; + this.sc = 0; + this.__v_skip = true; + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link2 = this.activeLink; + if (link2 === void 0 || link2.sub !== activeSub) { + link2 = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link2; + } else { + link2.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link2; + activeSub.depsTail = link2; + } + addSub(link2); + } else if (link2.version === -1) { + link2.version = this.version; + if (link2.nextDep) { + const next = link2.nextDep; + next.prevDep = link2.prevDep; + if (link2.prevDep) { + link2.prevDep.nextDep = next; + } + link2.prevDep = activeSub.depsTail; + link2.nextDep = void 0; + activeSub.depsTail.nextDep = link2; + activeSub.depsTail = link2; + if (activeSub.deps === link2) { + activeSub.deps = next; + } + } + } + return link2; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (false) ; + for (let link2 = this.subs; link2; link2 = link2.prevSub) { + if (link2.sub.notify()) { + ; + link2.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } +} +function addSub(link2) { + link2.dep.sc++; + if (link2.sub.flags & 4) { + const computed2 = link2.dep.computed; + if (computed2 && !link2.dep.subs) { + computed2.flags |= 4 | 16; + for (let l = computed2.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link2.dep.subs; + if (currentTail !== link2) { + link2.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link2; + } + link2.dep.subs = link2; + } +} +const targetMap = /* @__PURE__ */ new WeakMap(); +const ITERATE_KEY = /* @__PURE__ */ Symbol( + "" +); +const MAP_KEY_ITERATE_KEY = /* @__PURE__ */ Symbol( + "" +); +const ARRAY_ITERATE_KEY = /* @__PURE__ */ Symbol( + "" +); +function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + { + dep.track(); + } + } +} +function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + { + dep.trigger(); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); +} +function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); +} +function reactiveReadArray(array) { + const raw = /* @__PURE__ */ toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return /* @__PURE__ */ isShallow(array) ? raw : raw.map(toReactive); +} +function shallowReadArray(arr) { + track(arr = /* @__PURE__ */ toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; +} +function toWrapped(target, item) { + if (/* @__PURE__ */ isReadonly(target)) { + return /* @__PURE__ */ isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item); + } + return toReactive(item); +} +const arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, (item) => toWrapped(this, item)); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toWrapped(this, value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply( + this, + "filter", + fn, + thisArg, + (v) => v.map((item) => toWrapped(this, item)), + arguments + ); + }, + find(fn, thisArg) { + return apply( + this, + "find", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply( + this, + "findLast", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimization required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", (item) => toWrapped(this, item)); + } +}; +function iterator(self2, method, wrapValue) { + const arr = shallowReadArray(self2); + const iter = arr[method](); + if (arr !== self2 && !/* @__PURE__ */ isShallow(self2)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (!result.done) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; +} +const arrayProto = Array.prototype; +function apply(self2, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !/* @__PURE__ */ isShallow(self2); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self2, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self2) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self2); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; +} +function reduce(self2, method, fn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !/* @__PURE__ */ isShallow(self2); + let wrappedFn = fn; + let wrapInitialAccumulator = false; + if (arr !== self2) { + if (needsWrap) { + wrapInitialAccumulator = args.length === 0; + wrappedFn = function(acc, item, index) { + if (wrapInitialAccumulator) { + wrapInitialAccumulator = false; + acc = toWrapped(self2, acc); + } + return fn.call(this, acc, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self2); + }; + } + } + const result = arr[method](wrappedFn, ...args); + return wrapInitialAccumulator ? toWrapped(self2, result) : result; +} +function searchProxy(self2, method, args) { + const arr = /* @__PURE__ */ toRaw(self2); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && /* @__PURE__ */ isProxy(args[0])) { + args[0] = /* @__PURE__ */ toRaw(args[0]); + return arr[method](...args); + } + return res; +} +function noTracking(self2, method, args = []) { + pauseTracking(); + startBatch(); + const res = (/* @__PURE__ */ toRaw(self2))[method].apply(self2, args); + endBatch(); + resetTracking(); + return res; +} +const isNonTrackableKeys = /* @__PURE__ */ makeMap(`__proto__,__v_isRef,__isVue`); +const builtInSymbols = new Set( + /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) +); +function hasOwnProperty(key) { + if (!isSymbol(key)) key = String(key); + const obj = /* @__PURE__ */ toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); +} +class BaseReactiveHandler { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + /* @__PURE__ */ isRef(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (/* @__PURE__ */ isRef(res)) { + const value = targetIsArray && isIntegerKey(key) ? res : res.value; + return isReadonly2 && isObject$1(value) ? /* @__PURE__ */ readonly(value) : value; + } + if (isObject$1(res)) { + return isReadonly2 ? /* @__PURE__ */ readonly(res) : /* @__PURE__ */ reactive(res); + } + return res; + } +} +class MutableReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + const isArrayWithIntegerKey = isArray(target) && isIntegerKey(key); + if (!this._isShallow) { + const isOldValueReadonly = /* @__PURE__ */ isReadonly(oldValue); + if (!/* @__PURE__ */ isShallow(value) && !/* @__PURE__ */ isReadonly(value)) { + oldValue = /* @__PURE__ */ toRaw(oldValue); + value = /* @__PURE__ */ toRaw(value); + } + if (!isArrayWithIntegerKey && /* @__PURE__ */ isRef(oldValue) && !/* @__PURE__ */ isRef(value)) { + if (isOldValueReadonly) { + return true; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArrayWithIntegerKey ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + /* @__PURE__ */ isRef(target) ? target : receiver + ); + if (target === /* @__PURE__ */ toRaw(receiver) && result) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } +} +class ReadonlyReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + return true; + } + deleteProperty(target, key) { + return true; + } +} +const mutableHandlers = /* @__PURE__ */ new MutableReactiveHandler(); +const readonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(); +const shallowReactiveHandlers = /* @__PURE__ */ new MutableReactiveHandler(true); +const shallowReadonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(true); +const toShallow = (value) => value; +const getProto = (v) => Reflect.getPrototypeOf(v); +function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = /* @__PURE__ */ toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return extend( + // inheriting all iterator properties + Object.create(innerIterator), + { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + } + } + ); + }; +} +function createReadonlyMethod(type) { + return function(...args) { + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; +} +function createInstrumentations(readonly2, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = /* @__PURE__ */ toRaw(target); + const rawKey = /* @__PURE__ */ toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly2 && track(/* @__PURE__ */ toRaw(target), "iterate", ITERATE_KEY); + return target.size; + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = /* @__PURE__ */ toRaw(target); + const rawKey = /* @__PURE__ */ toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = /* @__PURE__ */ toRaw(target); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + !readonly2 && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly2 ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + const target = /* @__PURE__ */ toRaw(this); + const proto = getProto(target); + const rawValue = /* @__PURE__ */ toRaw(value); + const valueToAdd = !shallow && !/* @__PURE__ */ isShallow(value) && !/* @__PURE__ */ isReadonly(value) ? rawValue : value; + const hadKey = proto.has.call(target, valueToAdd) || hasChanged(value, valueToAdd) && proto.has.call(target, value) || hasChanged(rawValue, valueToAdd) && proto.has.call(target, rawValue); + if (!hadKey) { + target.add(valueToAdd); + trigger(target, "add", valueToAdd, valueToAdd); + } + return this; + }, + set(key, value) { + if (!shallow && !/* @__PURE__ */ isShallow(value) && !/* @__PURE__ */ isReadonly(value)) { + value = /* @__PURE__ */ toRaw(value); + } + const target = /* @__PURE__ */ toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = /* @__PURE__ */ toRaw(key); + hadKey = has.call(target, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value); + } + return this; + }, + delete(key) { + const target = /* @__PURE__ */ toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = /* @__PURE__ */ toRaw(key); + hadKey = has.call(target, key); + } + get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0); + } + return result; + }, + clear() { + const target = /* @__PURE__ */ toRaw(this); + const hadItems = target.size !== 0; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0 + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly2, shallow); + }); + return instrumentations; +} +function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; +} +const mutableCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, false) +}; +const shallowCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, true) +}; +const readonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, false) +}; +const shallowReadonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, true) +}; +const reactiveMap = /* @__PURE__ */ new WeakMap(); +const shallowReactiveMap = /* @__PURE__ */ new WeakMap(); +const readonlyMap = /* @__PURE__ */ new WeakMap(); +const shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); +function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2; + default: + return 0; + } +} +// @__NO_SIDE_EFFECTS__ +function reactive(target) { + if (/* @__PURE__ */ isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); +} +// @__NO_SIDE_EFFECTS__ +function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); +} +// @__NO_SIDE_EFFECTS__ +function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); +} +// @__NO_SIDE_EFFECTS__ +function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); +} +function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject$1(target)) { + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + if (target["__v_skip"] || !Object.isExtensible(target)) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const targetType = targetTypeMap(toRawType(target)); + if (targetType === 0) { + return target; + } + const proxy = new Proxy( + target, + targetType === 2 ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; +} +// @__NO_SIDE_EFFECTS__ +function isReactive(value) { + if (/* @__PURE__ */ isReadonly(value)) { + return /* @__PURE__ */ isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); +} +// @__NO_SIDE_EFFECTS__ +function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); +} +// @__NO_SIDE_EFFECTS__ +function isShallow(value) { + return !!(value && value["__v_isShallow"]); +} +// @__NO_SIDE_EFFECTS__ +function isProxy(value) { + return value ? !!value["__v_raw"] : false; +} +// @__NO_SIDE_EFFECTS__ +function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? /* @__PURE__ */ toRaw(raw) : observed; +} +function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; +} +const toReactive = (value) => isObject$1(value) ? /* @__PURE__ */ reactive(value) : value; +const toReadonly = (value) => isObject$1(value) ? /* @__PURE__ */ readonly(value) : value; +// @__NO_SIDE_EFFECTS__ +function isRef(r) { + return r ? r["__v_isRef"] === true : false; +} +// @__NO_SIDE_EFFECTS__ +function ref(value) { + return createRef(value, false); +} +// @__NO_SIDE_EFFECTS__ +function shallowRef(value) { + return createRef(value, true); +} +function createRef(rawValue, shallow) { + if (/* @__PURE__ */ isRef(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +class RefImpl { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : /* @__PURE__ */ toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + { + this.dep.track(); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || /* @__PURE__ */ isShallow(newValue) || /* @__PURE__ */ isReadonly(newValue); + newValue = useDirectValue ? newValue : /* @__PURE__ */ toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + { + this.dep.trigger(); + } + } + } +} +function unref(ref2) { + return /* @__PURE__ */ isRef(ref2) ? ref2.value : ref2; +} +function toValue(source) { + return isFunction(source) ? source() : unref(source); +} +const shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (/* @__PURE__ */ isRef(oldValue) && !/* @__PURE__ */ isRef(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } +}; +function proxyRefs(objectWithRefs) { + return /* @__PURE__ */ isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); +} +class CustomRefImpl { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } +} +function customRef(factory) { + return new CustomRefImpl(factory); +} +class ObjectRefImpl { + constructor(_object, key, _defaultValue) { + this._object = _object; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + this._key = isSymbol(key) ? key : String(key); + this._raw = /* @__PURE__ */ toRaw(_object); + let shallow = true; + let obj = _object; + if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) { + do { + shallow = !/* @__PURE__ */ isProxy(obj) || /* @__PURE__ */ isShallow(obj); + } while (shallow && (obj = obj["__v_raw"])); + } + this._shallow = shallow; + } + get value() { + let val = this._object[this._key]; + if (this._shallow) { + val = unref(val); + } + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + if (this._shallow && /* @__PURE__ */ isRef(this._raw[this._key])) { + const nestedRef = this._object[this._key]; + if (/* @__PURE__ */ isRef(nestedRef)) { + nestedRef.value = newVal; + return; + } + } + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(this._raw, this._key); + } +} +class GetterRefImpl { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } +} +// @__NO_SIDE_EFFECTS__ +function toRef$1(source, key, defaultValue) { + if (/* @__PURE__ */ isRef(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject$1(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return /* @__PURE__ */ ref(source); + } +} +function propertyToRef(source, key, defaultValue) { + return new ObjectRefImpl(source, key, defaultValue); +} +class ComputedRefImpl { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + this._value = void 0; + this.dep = new Dep(this); + this.__v_isRef = true; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 16; + this.globalVersion = globalVersion - 1; + this.next = void 0; + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } + } + get value() { + const link2 = this.dep.track(); + refreshComputed(this); + if (link2) { + link2.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } + } +} +// @__NO_SIDE_EFFECTS__ +function computed$1(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + return cRef; +} +const INITIAL_WATCHER_VALUE = {}; +const cleanupMap = /* @__PURE__ */ new WeakMap(); +let activeWatcher = void 0; +function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } +} +function watch$1(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (/* @__PURE__ */ isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect2; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (/* @__PURE__ */ isRef(source)) { + getter = () => source.value; + forceTrigger = /* @__PURE__ */ isShallow(source); + } else if (/* @__PURE__ */ isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => /* @__PURE__ */ isReactive(s) || /* @__PURE__ */ isShallow(s)); + getter = () => source.map((s) => { + if (/* @__PURE__ */ isRef(s)) { + return s.value; + } else if (/* @__PURE__ */ isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else ; + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect2; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect2.stop(); + if (scope && scope.active) { + remove(scope.effects, effect2); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + const res = _cb(...args); + watchHandle(); + return res; + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect2.flags & 1) || !effect2.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect2.run(); + if (immediateFirstRun || deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect2; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + oldValue = newValue; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect2.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect2 = new ReactiveEffect(getter); + effect2.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect2); + cleanup = effect2.onStop = () => { + const cleanups = cleanupMap.get(effect2); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect2); + } + }; + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect2.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect2.run(); + } + watchHandle.pause = effect2.pause.bind(effect2); + watchHandle.resume = effect2.resume.bind(effect2); + watchHandle.stop = watchHandle; + return watchHandle; +} +function traverse(value, depth = Infinity, seen2) { + if (depth <= 0 || !isObject$1(value) || value["__v_skip"]) { + return value; + } + seen2 = seen2 || /* @__PURE__ */ new Map(); + if ((seen2.get(value) || 0) >= depth) { + return value; + } + seen2.set(value, depth); + depth--; + if (/* @__PURE__ */ isRef(value)) { + traverse(value.value, depth, seen2); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen2); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen2); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen2); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen2); + } + } + } + return value; +} +/** +* @vue/runtime-core v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +const stack = []; +let isWarning = false; +function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack.length ? stack[stack.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; +} +function getComponentTrace() { + let currentVNode = stack[stack.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; +} +function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; +} +function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; +} +function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; +} +function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (/* @__PURE__ */ isRef(value)) { + value = formatProp(key, /* @__PURE__ */ toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = /* @__PURE__ */ toRaw(value); + return raw ? value : [`${key}=`, value]; + } +} +function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } +} +function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } +} +function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = `https://vuejs.org/error-reference/#runtime-${type}`; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); +} +function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + if (throwInProd) { + throw err; + } else { + console.error(err); + } +} +const queue = []; +let flushIndex = -1; +const pendingPostFlushCbs = []; +let activePostFlushCbs = null; +let postFlushIndex = 0; +const resolvedPromise = /* @__PURE__ */ Promise.resolve(); +let currentFlushPromise = null; +function nextTick(fn) { + const p2 = currentFlushPromise || resolvedPromise; + return fn ? p2.then(this ? fn.bind(this) : fn) : p2; +} +function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; +} +function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } +} +function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } +} +function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); +} +function flushPreFlushCbs(instance, seen2, i = flushIndex + 1) { + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= -2; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= -2; + } + } + } +} +function flushPostFlushCbs(seen2) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (cb.flags & 4) { + cb.flags &= -2; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= -2; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } +} +const getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; +function flushJobs(seen2) { + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (false) ; + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= -2; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(); + } + } +} +let currentRenderingInstance = null; +let currentScopeId = null; +function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; +} +function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; +} +function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; +} +function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } +} +function provide(key, value) { + if (currentInstance) { + let provides = currentInstance.provides; + const parentProvides = currentInstance.parent && currentInstance.parent.provides; + if (parentProvides === provides) { + provides = currentInstance.provides = Object.create(parentProvides); + } + provides[key] = value; + } +} +function inject(key, defaultValue, treatDefaultAsFactory = false) { + const instance = getCurrentInstance(); + if (instance || currentApp) { + let provides = currentApp ? currentApp._context.provides : instance ? instance.parent == null || instance.ce ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides : void 0; + if (provides && key in provides) { + return provides[key]; + } else if (arguments.length > 1) { + return treatDefaultAsFactory && isFunction(defaultValue) ? defaultValue.call(instance && instance.proxy) : defaultValue; + } else ; + } +} +function hasInjectionContext() { + return !!(getCurrentInstance() || currentApp); +} +const ssrContextKey = /* @__PURE__ */ Symbol.for("v-scx"); +const useSSRContext = () => { + { + const ctx = inject(ssrContextKey); + return ctx; + } +}; +function watchEffect(effect2, options) { + return doWatch(effect2, null, options); +} +function watchPostEffect(effect2, options) { + return doWatch( + effect2, + null, + { flush: "post" } + ); +} +function watch(source, cb, options) { + return doWatch(source, cb, options); +} +function doWatch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, flush, once } = options; + const baseWatchOptions = extend({}, options); + const runsImmediately = cb && immediate || !cb && flush !== "post"; + let ssrCleanup; + if (isInSSRComponentSetup) { + if (flush === "sync") { + const ctx = useSSRContext(); + ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []); + } else if (!runsImmediately) { + const watchStopHandle = () => { + }; + watchStopHandle.stop = NOOP; + watchStopHandle.resume = NOOP; + watchStopHandle.pause = NOOP; + return watchStopHandle; + } + } + const instance = currentInstance; + baseWatchOptions.call = (fn, type, args) => callWithAsyncErrorHandling(fn, instance, type, args); + let isPre = false; + if (flush === "post") { + baseWatchOptions.scheduler = (job) => { + queuePostRenderEffect(job, instance && instance.suspense); + }; + } else if (flush !== "sync") { + isPre = true; + baseWatchOptions.scheduler = (job, isFirstRun) => { + if (isFirstRun) { + job(); + } else { + queueJob(job); + } + }; + } + baseWatchOptions.augmentJob = (job) => { + if (cb) { + job.flags |= 4; + } + if (isPre) { + job.flags |= 2; + if (instance) { + job.id = instance.uid; + job.i = instance; + } + } + }; + const watchHandle = watch$1(source, cb, baseWatchOptions); + if (isInSSRComponentSetup) { + if (ssrCleanup) { + ssrCleanup.push(watchHandle); + } else if (runsImmediately) { + watchHandle(); + } + } + return watchHandle; +} +function instanceWatch(source, value, options) { + const publicThis = this.proxy; + const getter = isString(source) ? source.includes(".") ? createPathGetter(publicThis, source) : () => publicThis[source] : source.bind(publicThis, publicThis); + let cb; + if (isFunction(value)) { + cb = value; + } else { + cb = value.handler; + options = value; + } + const reset = setCurrentInstance(this); + const res = doWatch(getter, cb.bind(publicThis), options); + reset(); + return res; +} +function createPathGetter(ctx, path) { + const segments = path.split("."); + return () => { + let cur = ctx; + for (let i = 0; i < segments.length && cur; i++) { + cur = cur[segments[i]]; + } + return cur; + }; +} +const pendingMounts = /* @__PURE__ */ new WeakMap(); +const TeleportEndKey = /* @__PURE__ */ Symbol("_vte"); +const isTeleport = (type) => type.__isTeleport; +const isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); +const isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); +const isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; +const isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; +const resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + return null; + } else { + const target = select(targetSelector); + return target; + } + } else { + return targetSelector; + } +}; +const TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment, parentNode } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { dynamicChildren } = n2; + const mount = (vnode, container2, anchor2) => { + if (vnode.shapeFlag & 16) { + mountChildren( + vnode.children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = (vnode = n2) => { + const disabled2 = isTeleportDisabled(vnode.props); + const target = vnode.target = resolveTarget(vnode.props, querySelector); + const targetAnchor = prepareAnchor(target, vnode, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (parentComponent && parentComponent.isCE) { + (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target); + } + if (!disabled2) { + mount(vnode, target, targetAnchor); + updateCssVars(vnode, false); + } + } + }; + const queuePendingMount = (vnode) => { + const mountJob = () => { + if (pendingMounts.get(vnode) !== mountJob) return; + pendingMounts.delete(vnode); + if (isTeleportDisabled(vnode.props)) { + const mountContainer = parentNode(vnode.el) || container; + mount(vnode, mountContainer, vnode.anchor); + updateCssVars(vnode, true); + } + mountToTarget(vnode); + }; + pendingMounts.set(vnode, mountJob); + queuePostRenderEffect(mountJob, parentSuspense); + }; + if (n1 == null) { + const placeholder = n2.el = createText(""); + const mainAnchor = n2.anchor = createText(""); + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) { + queuePendingMount(n2); + return; + } + if (disabled) { + mount(n2, container, mainAnchor); + updateCssVars(n2, true); + } + mountToTarget(); + } else { + n2.el = n1.el; + const mainAnchor = n2.anchor = n1.anchor; + const pendingMount = pendingMounts.get(n1); + if (pendingMount) { + pendingMount.flags |= 8; + pendingMounts.delete(n1); + queuePendingMount(n2); + return; + } + n2.targetStart = n1.targetStart; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, true); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = resolveTarget(n2.props, querySelector); + if (nextTarget) { + n2.target = nextTarget; + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + const disabled = isTeleportDisabled(props); + const shouldRemove = doRemove || !disabled; + const pendingMount = pendingMounts.get(vnode); + if (pendingMount) { + pendingMount.flags |= 8; + pendingMounts.delete(vnode); + } + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (!pendingMount && (disabled || target) && shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport +}; +function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!pendingMounts.has(vnode) && (!isReorder || isTeleportDisabled(props))) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } +} +function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } +}, hydrateChildren) { + function hydrateAnchor(target2, targetNode) { + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target2._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + } + function hydrateDisabledTeleport(node2, vnode2) { + vnode2.anchor = hydrateChildren( + nextSibling(node2), + vnode2, + parentNode(node2), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + const disabled = isTeleportDisabled(vnode.props); + if (target) { + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + hydrateDisabledTeleport(node, vnode); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor( + target, + vnode, + createText, + insert, + // if target is the same as the main view, insert anchors before current node + // to avoid hydrating mismatch + parentNode(node) === target ? node : null + ); + } + } else { + vnode.anchor = nextSibling(node); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } else if (disabled) { + if (vnode.shapeFlag & 16) { + hydrateDisabledTeleport(node, vnode); + vnode.targetStart = node; + vnode.targetAnchor = nextSibling(node); + } + } + return vnode.anchor && nextSibling(vnode.anchor); +} +const Teleport = TeleportImpl; +function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } +} +function prepareAnchor(target, vnode, createText, insert, anchor = null) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target, anchor); + insert(targetAnchor, target, anchor); + } + return targetAnchor; +} +const leaveCbKey = /* @__PURE__ */ Symbol("_leaveCb"); +const enterCbKey = /* @__PURE__ */ Symbol("_enterCb"); +function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; +} +const TransitionHookValidator = [Function, Array]; +const BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator +}; +const recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; +}; +const BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + const child = children && children.length ? findNonCommentChild(children) : ( + // Keep explicit default-slot conditionals on the same transition path + // as regular v-if branches, which render a comment placeholder. + instance.subTree ? createCommentVNode() : void 0 + ); + if (!child) { + return; + } + const rawProps = /* @__PURE__ */ toRaw(props); + const { mode } = rawProps; + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(oldInnerChild, innerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } +}; +function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + for (const c of children) { + if (c.type !== Comment) { + child = c; + break; + } + } + } + return child; +} +const BaseTransition = BaseTransitionImpl; +function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; +} +function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook2 = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook2(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook2(hook, [el]); + }, + enter(el) { + if (leavingVNodesCache[key] === vnode) return; + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + el[enterCbKey] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook2(cancelHook, [el]); + } else { + callHook2(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey] = void 0; + }; + const done = el[enterCbKey].bind(null, false); + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove2) { + const key2 = String(vnode.key); + if (el[enterCbKey]) { + el[enterCbKey]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove2(); + } + callHook2(onBeforeLeave, [el]); + let called = false; + el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove2(); + if (cancelled) { + callHook2(onLeaveCancelled, [el]); + } else { + callHook2(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + const done = el[leaveCbKey].bind(null, false); + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; +} +function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } +} +function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } +} +function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } +} +function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; +} +// @__NO_SIDE_EFFECTS__ +function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; +} +function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; +} +function isTemplateRefKey(refs, key) { + let desc; + return !!((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable); +} +const pendingSetRefMap = /* @__PURE__ */ new WeakMap(); +function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref3 } = rawRef; + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = /* @__PURE__ */ toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? NO : (key) => { + if (isTemplateRefKey(refs, key)) { + return false; + } + return hasOwn(rawSetupState, key); + }; + const canSetRef = (ref22, key) => { + if (key && isTemplateRefKey(refs, key)) { + return false; + } + return true; + }; + if (oldRef != null && oldRef !== ref3) { + invalidatePendingSetRef(oldRawRef); + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (/* @__PURE__ */ isRef(oldRef)) { + const oldRawRefAtom = oldRawRef; + if (canSetRef(oldRef, oldRawRefAtom.k)) { + oldRef.value = null; + } + if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null; + } + } + if (isFunction(ref3)) { + pauseTracking(); + try { + callWithErrorHandling(ref3, owner, 12, [value, refs]); + } finally { + resetTracking(); + } + } else { + const _isString = isString(ref3); + const _isRef = /* @__PURE__ */ isRef(ref3); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref3) ? setupState[ref3] : refs[ref3] : canSetRef() || !rawRef.k ? ref3.value : refs[rawRef.k]; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref3] = [refValue]; + if (canSetSetupRef(ref3)) { + setupState[ref3] = refs[ref3]; + } + } else { + const newVal = [refValue]; + if (canSetRef(ref3, rawRef.k)) { + ref3.value = newVal; + } + if (rawRef.k) refs[rawRef.k] = newVal; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref3] = value; + if (canSetSetupRef(ref3)) { + setupState[ref3] = value; + } + } else if (_isRef) { + if (canSetRef(ref3, rawRef.k)) { + ref3.value = value; + } + if (rawRef.k) refs[rawRef.k] = value; + } else ; + }; + if (value) { + const job = () => { + doSet(); + pendingSetRefMap.delete(rawRef); + }; + job.id = -1; + pendingSetRefMap.set(rawRef, job); + queuePostRenderEffect(job, parentSuspense); + } else { + invalidatePendingSetRef(rawRef); + doSet(); + } + } + } +} +function invalidatePendingSetRef(rawRef) { + const pendingSetRef = pendingSetRefMap.get(rawRef); + if (pendingSetRef) { + pendingSetRef.flags |= 8; + pendingSetRefMap.delete(rawRef); + } +} +let hasLoggedMismatchError = false; +const logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; +}; +const isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; +const isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); +const getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; +}; +const isComment = (node) => node.nodeType === 8; +function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp: patchProp2, + createText, + nextSibling, + parentNode, + remove: remove2, + insert, + createComment + } + } = rendererInternals; + const hydrate = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref: ref3, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref3 != null) { + setRef(ref3, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { + type, + dynamicProps, + props, + patchFlag, + shapeFlag, + dirs, + transition + } = vnode; + const forcePatch = type === "input" || type === "option"; + const hasDynamicProps = !!dynamicProps; + if (forcePatch || hasDynamicProps || patchFlag !== -1) { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + const cls = content.getAttribute("class"); + if (cls) content.$cls = cls; + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && !isMismatchAllowed( + el, + 1 + /* CHILDREN */ + )) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + logMismatchError(); + } + while (next) { + const cur = next; + next = next.nextSibling; + remove2(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + const { textContent } = el; + if (textContent !== clientText && // innerHTML normalize \r\n or \r into a single \n in the DOM + textContent !== clientText.replace(/\r\n|\r/g, "\n")) { + if (!isMismatchAllowed( + el, + 0 + /* TEXT */ + )) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${textContent} + - expected on client: ${clientText}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if ( + // #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent) + ) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement && !isReservedProp(key) || dynamicProps && dynamicProps.includes(key)) { + patchProp2(el, key, null, props[key], void 0, parentComponent); + } + } + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasCheckedMismatch = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!hasCheckedMismatch) { + hasCheckedMismatch = true; + if (!isMismatchAllowed( + container, + 1 + /* CHILDREN */ + )) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + logMismatchError(); + } + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isNodeMismatchAllowed(node, vnode)) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove2(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove2(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate, hydrateNode]; +} +function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + if (el.$cls) { + actual = el.$cls; + delete el.$cls; + } else { + actual = el.getAttribute("class"); + } + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; +} +function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); +} +function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; +} +function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; +} +function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; +} +function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]); + expectedMap.set(`--${getEscapedCssVarName(key)}`, value); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } +} +const allowMismatchAttr = "data-allow-mismatch"; +const MismatchTypeString = { + [ + 0 + /* TEXT */ + ]: "text", + [ + 1 + /* CHILDREN */ + ]: "children", + [ + 2 + /* CLASS */ + ]: "class", + [ + 3 + /* STYLE */ + ]: "style", + [ + 4 + /* ATTRIBUTE */ + ]: "attribute" +}; +function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 || allowedType === 1) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + return isMismatchAllowedByAttr( + el && el.getAttribute(allowMismatchAttr), + allowedType + ); +} +function isMismatchAllowedByAttr(allowedAttr, allowedType) { + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 && list.includes("children")) { + return true; + } + return list.includes(MismatchTypeString[allowedType]); + } +} +function isNodeMismatchAllowed(node, vnode) { + return isMismatchAllowed( + node.parentElement, + 1 + /* CHILDREN */ + ) || isMismatchAllowedByNode(node) || isMismatchAllowedByVNode(vnode); +} +function isMismatchAllowedByNode(node) { + return node.nodeType === 1 && isMismatchAllowedByAttr( + node.getAttribute(allowMismatchAttr), + 1 + /* CHILDREN */ + ); +} +function isMismatchAllowedByVNode({ props }) { + const allowedAttr = props && props[allowMismatchAttr]; + return typeof allowedAttr === "string" && isMismatchAllowedByAttr( + allowedAttr, + 1 + /* CHILDREN */ + ); +} +getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); +getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); +function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } +} +const isAsyncWrapper = (i) => !!i.type.__asyncLoader; +// @__NO_SIDE_EFFECTS__ +function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve2, reject) => { + const userRetry = () => resolve2(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + resolvedComp = comp; + return comp; + })); + }; + return /* @__PURE__ */ defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate) { + let patched = false; + (instance.bu || (instance.bu = [])).push(() => patched = true); + const performHydrate = () => { + if (patched) { + return; + } + hydrate(); + }; + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + performHydrate, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : performHydrate; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || isInSSRComponentSetup) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = /* @__PURE__ */ ref(false); + const error = /* @__PURE__ */ ref(); + const delayed = /* @__PURE__ */ ref(!!delay); + let timeoutTimer; + let delayTimer; + onUnmounted(() => { + if (timeoutTimer != null) clearTimeout(timeoutTimer); + if (delayTimer != null) clearTimeout(delayTimer); + }); + if (delay) { + delayTimer = setTimeout(() => { + if (instance.isUnmounted) return; + delayed.value = false; + }, delay); + } + if (timeout != null) { + timeoutTimer = setTimeout(() => { + if (instance.isUnmounted) return; + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + if (instance.isUnmounted) return; + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + if (instance.isUnmounted) { + pendingRequest = null; + return; + } + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createInnerComp( + loadingComponent, + instance + ); + } + }; + } + }); +} +function createInnerComp(comp, parent) { + const { ref: ref22, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref22; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; +} +const isKeepAlive = (vnode) => vnode.type.__isKeepAlive; +function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); +} +function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); +} +function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } +} +function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); +} +function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } +} +const createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } +}; +const onBeforeMount = createHook("bm"); +const onMounted = createHook("m"); +const onBeforeUpdate = createHook( + "bu" +); +const onUpdated = createHook("u"); +const onBeforeUnmount = createHook( + "bum" +); +const onUnmounted = createHook("um"); +const onServerPrefetch = createHook( + "sp" +); +const onRenderTriggered = createHook("rtg"); +const onRenderTracked = createHook("rtc"); +function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); +} +const COMPONENTS = "components"; +function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; +} +const NULL_DYNAMIC_COMPONENT = /* @__PURE__ */ Symbol.for("v-ndc"); +function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } +} +function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + return res; + } +} +function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); +} +function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && /* @__PURE__ */ isReactive(source); + let needsWrap = false; + let isReadonlySource = false; + if (sourceIsReactiveArray) { + needsWrap = !/* @__PURE__ */ isShallow(source); + isReadonlySource = /* @__PURE__ */ isReadonly(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i], + i, + void 0, + cached + ); + } + } else if (typeof source === "number") { + { + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached); + } + } + } else if (isObject$1(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached); + } + } + } else { + ret = []; + } + return ret; +} +function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + const hasProps = Object.keys(props).length > 0; + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + hasProps ? -2 : 64 + ); + } + let slot = slots[name]; + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; +} +function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; +} +function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + for (const key in obj) { + ret[/[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; +} +const getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); +}; +const publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + /* @__PURE__ */ extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => i.props, + $attrs: (i) => i.attrs, + $slots: (i) => i.slots, + $refs: (i) => i.refs, + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => resolveMergedOptions(i), + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => instanceWatch.bind(i) + }) +); +const hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); +const PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1: + return setupState[key]; + case 2: + return data[key]; + case 4: + return ctx[key]; + case 3: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1; + return setupState[key]; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2; + return data[key]; + } else if (hasOwn(props, key)) { + accessCache[key] = 3; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if (shouldCacheAccess) { + accessCache[key] = 0; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else ; + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + data[key] = value; + return true; + } else if (hasOwn(instance.props, key)) { + return false; + } + if (key[0] === "$" && key.slice(1) in instance) { + return false; + } else { + { + ctx[key] = value; + } + } + return true; + }, + has({ + _: { data, setupState, accessCache, ctx, appContext, props, type } + }, key) { + let cssModules; + return !!(accessCache[key] || data !== EMPTY_OBJ && key[0] !== "$" && hasOwn(data, key) || hasSetupBinding(setupState, key) || hasOwn(props, key) || hasOwn(ctx, key) || hasOwn(publicPropertiesMap, key) || hasOwn(appContext.config.globalProperties, key) || (cssModules = type.__cssModules) && cssModules[key]); + }, + defineProperty(target, key, descriptor) { + if (descriptor.get != null) { + target._.accessCache[key] = 0; + } else if (hasOwn(descriptor, "value")) { + this.set(target, key, descriptor.value, null); + } + return Reflect.defineProperty(target, key, descriptor); + } +}; +function useSlots() { + return getContext().slots; +} +function getContext(calledFunctionName) { + const i = getCurrentInstance(); + return i.setupContext || (i.setupContext = createSetupContext(i)); +} +function normalizePropsOrEmits(props) { + return isArray(props) ? props.reduce( + (normalized, p2) => (normalized[p2] = null, normalized), + {} + ) : props; +} +let shouldCacheAccess = true; +function applyOptions(instance) { + const options = resolveMergedOptions(instance); + const publicThis = instance.proxy; + const ctx = instance.ctx; + shouldCacheAccess = false; + if (options.beforeCreate) { + callHook$1(options.beforeCreate, instance, "bc"); + } + const { + // state + data: dataOptions, + computed: computedOptions, + methods, + watch: watchOptions, + provide: provideOptions, + inject: injectOptions, + // lifecycle + created, + beforeMount, + mounted, + beforeUpdate, + updated, + activated, + deactivated, + beforeDestroy, + beforeUnmount, + destroyed, + unmounted, + render, + renderTracked, + renderTriggered, + errorCaptured, + serverPrefetch, + // public API + expose, + inheritAttrs, + // assets + components, + directives, + filters + } = options; + const checkDuplicateProperties = null; + if (injectOptions) { + resolveInjections(injectOptions, ctx, checkDuplicateProperties); + } + if (methods) { + for (const key in methods) { + const methodHandler = methods[key]; + if (isFunction(methodHandler)) { + { + ctx[key] = methodHandler.bind(publicThis); + } + } + } + } + if (dataOptions) { + const data = dataOptions.call(publicThis, publicThis); + if (!isObject$1(data)) ; + else { + instance.data = /* @__PURE__ */ reactive(data); + } + } + shouldCacheAccess = true; + if (computedOptions) { + for (const key in computedOptions) { + const opt = computedOptions[key]; + const get = isFunction(opt) ? opt.bind(publicThis, publicThis) : isFunction(opt.get) ? opt.get.bind(publicThis, publicThis) : NOOP; + const set = !isFunction(opt) && isFunction(opt.set) ? opt.set.bind(publicThis) : NOOP; + const c = computed({ + get, + set + }); + Object.defineProperty(ctx, key, { + enumerable: true, + configurable: true, + get: () => c.value, + set: (v) => c.value = v + }); + } + } + if (watchOptions) { + for (const key in watchOptions) { + createWatcher(watchOptions[key], ctx, publicThis, key); + } + } + if (provideOptions) { + const provides = isFunction(provideOptions) ? provideOptions.call(publicThis) : provideOptions; + Reflect.ownKeys(provides).forEach((key) => { + provide(key, provides[key]); + }); + } + if (created) { + callHook$1(created, instance, "c"); + } + function registerLifecycleHook(register, hook) { + if (isArray(hook)) { + hook.forEach((_hook) => register(_hook.bind(publicThis))); + } else if (hook) { + register(hook.bind(publicThis)); + } + } + registerLifecycleHook(onBeforeMount, beforeMount); + registerLifecycleHook(onMounted, mounted); + registerLifecycleHook(onBeforeUpdate, beforeUpdate); + registerLifecycleHook(onUpdated, updated); + registerLifecycleHook(onActivated, activated); + registerLifecycleHook(onDeactivated, deactivated); + registerLifecycleHook(onErrorCaptured, errorCaptured); + registerLifecycleHook(onRenderTracked, renderTracked); + registerLifecycleHook(onRenderTriggered, renderTriggered); + registerLifecycleHook(onBeforeUnmount, beforeUnmount); + registerLifecycleHook(onUnmounted, unmounted); + registerLifecycleHook(onServerPrefetch, serverPrefetch); + if (isArray(expose)) { + if (expose.length) { + const exposed = instance.exposed || (instance.exposed = {}); + expose.forEach((key) => { + Object.defineProperty(exposed, key, { + get: () => publicThis[key], + set: (val) => publicThis[key] = val, + enumerable: true + }); + }); + } else if (!instance.exposed) { + instance.exposed = {}; + } + } + if (render && instance.render === NOOP) { + instance.render = render; + } + if (inheritAttrs != null) { + instance.inheritAttrs = inheritAttrs; + } + if (components) instance.components = components; + if (directives) instance.directives = directives; + if (serverPrefetch) { + markAsyncBoundary(instance); + } +} +function resolveInjections(injectOptions, ctx, checkDuplicateProperties = NOOP) { + if (isArray(injectOptions)) { + injectOptions = normalizeInject(injectOptions); + } + for (const key in injectOptions) { + const opt = injectOptions[key]; + let injected; + if (isObject$1(opt)) { + if ("default" in opt) { + injected = inject( + opt.from || key, + opt.default, + true + ); + } else { + injected = inject(opt.from || key); + } + } else { + injected = inject(opt); + } + if (/* @__PURE__ */ isRef(injected)) { + Object.defineProperty(ctx, key, { + enumerable: true, + configurable: true, + get: () => injected.value, + set: (v) => injected.value = v + }); + } else { + ctx[key] = injected; + } + } +} +function callHook$1(hook, instance, type) { + callWithAsyncErrorHandling( + isArray(hook) ? hook.map((h2) => h2.bind(instance.proxy)) : hook.bind(instance.proxy), + instance, + type + ); +} +function createWatcher(raw, ctx, publicThis, key) { + let getter = key.includes(".") ? createPathGetter(publicThis, key) : () => publicThis[key]; + if (isString(raw)) { + const handler = ctx[raw]; + if (isFunction(handler)) { + { + watch(getter, handler); + } + } + } else if (isFunction(raw)) { + { + watch(getter, raw.bind(publicThis)); + } + } else if (isObject$1(raw)) { + if (isArray(raw)) { + raw.forEach((r) => createWatcher(r, ctx, publicThis, key)); + } else { + const handler = isFunction(raw.handler) ? raw.handler.bind(publicThis) : ctx[raw.handler]; + if (isFunction(handler)) { + watch(getter, handler, raw); + } + } + } else ; +} +function resolveMergedOptions(instance) { + const base = instance.type; + const { mixins, extends: extendsOptions } = base; + const { + mixins: globalMixins, + optionsCache: cache, + config: { optionMergeStrategies } + } = instance.appContext; + const cached = cache.get(base); + let resolved; + if (cached) { + resolved = cached; + } else if (!globalMixins.length && !mixins && !extendsOptions) { + { + resolved = base; + } + } else { + resolved = {}; + if (globalMixins.length) { + globalMixins.forEach( + (m) => mergeOptions(resolved, m, optionMergeStrategies, true) + ); + } + mergeOptions(resolved, base, optionMergeStrategies); + } + if (isObject$1(base)) { + cache.set(base, resolved); + } + return resolved; +} +function mergeOptions(to, from, strats, asMixin = false) { + const { mixins, extends: extendsOptions } = from; + if (extendsOptions) { + mergeOptions(to, extendsOptions, strats, true); + } + if (mixins) { + mixins.forEach( + (m) => mergeOptions(to, m, strats, true) + ); + } + for (const key in from) { + if (asMixin && key === "expose") ; + else { + const strat = internalOptionMergeStrats[key] || strats && strats[key]; + to[key] = strat ? strat(to[key], from[key]) : from[key]; + } + } + return to; +} +const internalOptionMergeStrats = { + data: mergeDataFn, + props: mergeEmitsOrPropsOptions, + emits: mergeEmitsOrPropsOptions, + // objects + methods: mergeObjectOptions, + computed: mergeObjectOptions, + // lifecycle + beforeCreate: mergeAsArray, + created: mergeAsArray, + beforeMount: mergeAsArray, + mounted: mergeAsArray, + beforeUpdate: mergeAsArray, + updated: mergeAsArray, + beforeDestroy: mergeAsArray, + beforeUnmount: mergeAsArray, + destroyed: mergeAsArray, + unmounted: mergeAsArray, + activated: mergeAsArray, + deactivated: mergeAsArray, + errorCaptured: mergeAsArray, + serverPrefetch: mergeAsArray, + // assets + components: mergeObjectOptions, + directives: mergeObjectOptions, + // watch + watch: mergeWatchOptions, + // provide / inject + provide: mergeDataFn, + inject: mergeInject +}; +function mergeDataFn(to, from) { + if (!from) { + return to; + } + if (!to) { + return from; + } + return function mergedDataFn() { + return extend( + isFunction(to) ? to.call(this, this) : to, + isFunction(from) ? from.call(this, this) : from + ); + }; +} +function mergeInject(to, from) { + return mergeObjectOptions(normalizeInject(to), normalizeInject(from)); +} +function normalizeInject(raw) { + if (isArray(raw)) { + const res = {}; + for (let i = 0; i < raw.length; i++) { + res[raw[i]] = raw[i]; + } + return res; + } + return raw; +} +function mergeAsArray(to, from) { + return to ? [...new Set([].concat(to, from))] : from; +} +function mergeObjectOptions(to, from) { + return to ? extend(/* @__PURE__ */ Object.create(null), to, from) : from; +} +function mergeEmitsOrPropsOptions(to, from) { + if (to) { + if (isArray(to) && isArray(from)) { + return [.../* @__PURE__ */ new Set([...to, ...from])]; + } + return extend( + /* @__PURE__ */ Object.create(null), + normalizePropsOrEmits(to), + normalizePropsOrEmits(from != null ? from : {}) + ); + } else { + return from; + } +} +function mergeWatchOptions(to, from) { + if (!to) return from; + if (!from) return to; + const merged = extend(/* @__PURE__ */ Object.create(null), to); + for (const key in from) { + merged[key] = mergeAsArray(to[key], from[key]); + } + return merged; +} +function createAppContext() { + return { + app: null, + config: { + isNativeTag: NO, + performance: false, + globalProperties: {}, + optionMergeStrategies: {}, + errorHandler: void 0, + warnHandler: void 0, + compilerOptions: {} + }, + mixins: [], + components: {}, + directives: {}, + provides: /* @__PURE__ */ Object.create(null), + optionsCache: /* @__PURE__ */ new WeakMap(), + propsCache: /* @__PURE__ */ new WeakMap(), + emitsCache: /* @__PURE__ */ new WeakMap() + }; +} +let uid$1 = 0; +function createAppAPI(render, hydrate) { + return function createApp2(rootComponent, rootProps = null) { + if (!isFunction(rootComponent)) { + rootComponent = extend({}, rootComponent); + } + if (rootProps != null && !isObject$1(rootProps)) { + rootProps = null; + } + const context = createAppContext(); + const installedPlugins = /* @__PURE__ */ new WeakSet(); + const pluginCleanupFns = []; + let isMounted = false; + const app = context.app = { + _uid: uid$1++, + _component: rootComponent, + _props: rootProps, + _container: null, + _context: context, + _instance: null, + version, + get config() { + return context.config; + }, + set config(v) { + }, + use(plugin, ...options) { + if (installedPlugins.has(plugin)) ; + else if (plugin && isFunction(plugin.install)) { + installedPlugins.add(plugin); + plugin.install(app, ...options); + } else if (isFunction(plugin)) { + installedPlugins.add(plugin); + plugin(app, ...options); + } else ; + return app; + }, + mixin(mixin) { + { + if (!context.mixins.includes(mixin)) { + context.mixins.push(mixin); + } + } + return app; + }, + component(name, component) { + if (!component) { + return context.components[name]; + } + context.components[name] = component; + return app; + }, + directive(name, directive) { + if (!directive) { + return context.directives[name]; + } + context.directives[name] = directive; + return app; + }, + mount(rootContainer, isHydrate, namespace) { + if (!isMounted) { + const vnode = app._ceVNode || createVNode(rootComponent, rootProps); + vnode.appContext = context; + if (namespace === true) { + namespace = "svg"; + } else if (namespace === false) { + namespace = void 0; + } + if (isHydrate && hydrate) { + hydrate(vnode, rootContainer); + } else { + render(vnode, rootContainer, namespace); + } + isMounted = true; + app._container = rootContainer; + rootContainer.__vue_app__ = app; + return getComponentPublicInstance(vnode.component); + } + }, + onUnmount(cleanupFn) { + pluginCleanupFns.push(cleanupFn); + }, + unmount() { + if (isMounted) { + callWithAsyncErrorHandling( + pluginCleanupFns, + app._instance, + 16 + ); + render(null, app._container); + delete app._container.__vue_app__; + } + }, + provide(key, value) { + context.provides[key] = value; + return app; + }, + runWithContext(fn) { + const lastApp = currentApp; + currentApp = app; + try { + return fn(); + } finally { + currentApp = lastApp; + } + } + }; + return app; + }; +} +let currentApp = null; +const getModelModifiers = (props, modelName) => { + return modelName === "modelValue" || modelName === "model-value" ? props.modelModifiers : props[`${modelName}Modifiers`] || props[`${camelize(modelName)}Modifiers`] || props[`${hyphenate(modelName)}Modifiers`]; +}; +function emit(instance, event, ...rawArgs) { + if (instance.isUnmounted) return; + const props = instance.vnode.props || EMPTY_OBJ; + let args = rawArgs; + const isModelListener2 = event.startsWith("update:"); + const modifiers = isModelListener2 && getModelModifiers(props, event.slice(7)); + if (modifiers) { + if (modifiers.trim) { + args = rawArgs.map((a) => isString(a) ? a.trim() : a); + } + if (modifiers.number) { + args = rawArgs.map(looseToNumber); + } + } + let handlerName; + let handler = props[handlerName = toHandlerKey(event)] || // also try camelCase event handler (#2249) + props[handlerName = toHandlerKey(camelize(event))]; + if (!handler && isModelListener2) { + handler = props[handlerName = toHandlerKey(hyphenate(event))]; + } + if (handler) { + callWithAsyncErrorHandling( + handler, + instance, + 6, + args + ); + } + const onceHandler = props[handlerName + `Once`]; + if (onceHandler) { + if (!instance.emitted) { + instance.emitted = {}; + } else if (instance.emitted[handlerName]) { + return; + } + instance.emitted[handlerName] = true; + callWithAsyncErrorHandling( + onceHandler, + instance, + 6, + args + ); + } +} +const mixinEmitsCache = /* @__PURE__ */ new WeakMap(); +function normalizeEmitsOptions(comp, appContext, asMixin = false) { + const cache = asMixin ? mixinEmitsCache : appContext.emitsCache; + const cached = cache.get(comp); + if (cached !== void 0) { + return cached; + } + const raw = comp.emits; + let normalized = {}; + let hasExtends = false; + if (!isFunction(comp)) { + const extendEmits = (raw2) => { + const normalizedFromExtend = normalizeEmitsOptions(raw2, appContext, true); + if (normalizedFromExtend) { + hasExtends = true; + extend(normalized, normalizedFromExtend); + } + }; + if (!asMixin && appContext.mixins.length) { + appContext.mixins.forEach(extendEmits); + } + if (comp.extends) { + extendEmits(comp.extends); + } + if (comp.mixins) { + comp.mixins.forEach(extendEmits); + } + } + if (!raw && !hasExtends) { + if (isObject$1(comp)) { + cache.set(comp, null); + } + return null; + } + if (isArray(raw)) { + raw.forEach((key) => normalized[key] = null); + } else { + extend(normalized, raw); + } + if (isObject$1(comp)) { + cache.set(comp, normalized); + } + return normalized; +} +function isEmitListener(options, key) { + if (!options || !isOn(key)) { + return false; + } + key = key.slice(2); + key = key === "Once" ? key : key.replace(/Once$/, ""); + return hasOwn(options, key[0].toLowerCase() + key.slice(1)) || hasOwn(options, hyphenate(key)) || hasOwn(options, key); +} +function markAttrsAccessed() { +} +function renderComponentRoot(instance) { + const { + type: Component, + vnode, + proxy, + withProxy, + propsOptions: [propsOptions], + slots, + attrs, + emit: emit2, + render, + renderCache, + props, + data, + setupState, + ctx, + inheritAttrs + } = instance; + const prev = setCurrentRenderingInstance(instance); + let result; + let fallthroughAttrs; + try { + if (vnode.shapeFlag & 4) { + const proxyToUse = withProxy || proxy; + const thisProxy = false ? new Proxy(proxyToUse, { + get(target, key, receiver) { + warn$1( + `Property '${String( + key + )}' was accessed via 'this'. Avoid using 'this' in templates.` + ); + return Reflect.get(target, key, receiver); + } + }) : proxyToUse; + result = normalizeVNode( + render.call( + thisProxy, + proxyToUse, + renderCache, + false ? /* @__PURE__ */ shallowReadonly(props) : props, + setupState, + data, + ctx + ) + ); + fallthroughAttrs = attrs; + } else { + const render2 = Component; + if (false) ; + result = normalizeVNode( + render2.length > 1 ? render2( + false ? /* @__PURE__ */ shallowReadonly(props) : props, + false ? { + get attrs() { + markAttrsAccessed(); + return /* @__PURE__ */ shallowReadonly(attrs); + }, + slots, + emit: emit2 + } : { attrs, slots, emit: emit2 } + ) : render2( + false ? /* @__PURE__ */ shallowReadonly(props) : props, + null + ) + ); + fallthroughAttrs = Component.props ? attrs : getFunctionalFallthrough(attrs); + } + } catch (err) { + blockStack.length = 0; + handleError(err, instance, 1); + result = createVNode(Comment); + } + let root = result; + if (fallthroughAttrs && inheritAttrs !== false) { + const keys = Object.keys(fallthroughAttrs); + const { shapeFlag } = root; + if (keys.length) { + if (shapeFlag & (1 | 6)) { + if (propsOptions && keys.some(isModelListener)) { + fallthroughAttrs = filterModelListeners( + fallthroughAttrs, + propsOptions + ); + } + root = cloneVNode(root, fallthroughAttrs, false, true); + } + } + } + if (vnode.dirs) { + root = cloneVNode(root, null, false, true); + root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs; + } + if (vnode.transition) { + setTransitionHooks(root, vnode.transition); + } + { + result = root; + } + setCurrentRenderingInstance(prev); + return result; +} +const getFunctionalFallthrough = (attrs) => { + let res; + for (const key in attrs) { + if (key === "class" || key === "style" || isOn(key)) { + (res || (res = {}))[key] = attrs[key]; + } + } + return res; +}; +const filterModelListeners = (attrs, props) => { + const res = {}; + for (const key in attrs) { + if (!isModelListener(key) || !(key.slice(9) in props)) { + res[key] = attrs[key]; + } + } + return res; +}; +function shouldUpdateComponent(prevVNode, nextVNode, optimized) { + const { props: prevProps, children: prevChildren, component } = prevVNode; + const { props: nextProps, children: nextChildren, patchFlag } = nextVNode; + const emits = component.emitsOptions; + if (nextVNode.dirs || nextVNode.transition) { + return true; + } + if (optimized && patchFlag >= 0) { + if (patchFlag & 1024) { + return true; + } + if (patchFlag & 16) { + if (!prevProps) { + return !!nextProps; + } + return hasPropsChanged(prevProps, nextProps, emits); + } else if (patchFlag & 8) { + const dynamicProps = nextVNode.dynamicProps; + for (let i = 0; i < dynamicProps.length; i++) { + const key = dynamicProps[i]; + if (hasPropValueChanged(nextProps, prevProps, key) && !isEmitListener(emits, key)) { + return true; + } + } + } + } else { + if (prevChildren || nextChildren) { + if (!nextChildren || !nextChildren.$stable) { + return true; + } + } + if (prevProps === nextProps) { + return false; + } + if (!prevProps) { + return !!nextProps; + } + if (!nextProps) { + return true; + } + return hasPropsChanged(prevProps, nextProps, emits); + } + return false; +} +function hasPropsChanged(prevProps, nextProps, emitsOptions) { + const nextKeys = Object.keys(nextProps); + if (nextKeys.length !== Object.keys(prevProps).length) { + return true; + } + for (let i = 0; i < nextKeys.length; i++) { + const key = nextKeys[i]; + if (hasPropValueChanged(nextProps, prevProps, key) && !isEmitListener(emitsOptions, key)) { + return true; + } + } + return false; +} +function hasPropValueChanged(nextProps, prevProps, key) { + const nextProp = nextProps[key]; + const prevProp = prevProps[key]; + if (key === "style" && isObject$1(nextProp) && isObject$1(prevProp)) { + return !looseEqual(nextProp, prevProp); + } + return nextProp !== prevProp; +} +function updateHOCHostEl({ vnode, parent, suspense }, el) { + while (parent) { + const root = parent.subTree; + if (root.suspense && root.suspense.activeBranch === vnode) { + root.suspense.vnode.el = root.el = el; + vnode = root; + } + if (root === vnode) { + (vnode = parent.vnode).el = el; + parent = parent.parent; + } else { + break; + } + } + if (suspense && suspense.activeBranch === vnode) { + suspense.vnode.el = el; + } +} +const internalObjectProto = {}; +const createInternalObject = () => Object.create(internalObjectProto); +const isInternalObject = (obj) => Object.getPrototypeOf(obj) === internalObjectProto; +function initProps(instance, rawProps, isStateful, isSSR = false) { + const props = {}; + const attrs = createInternalObject(); + instance.propsDefaults = /* @__PURE__ */ Object.create(null); + setFullProps(instance, rawProps, props, attrs); + for (const key in instance.propsOptions[0]) { + if (!(key in props)) { + props[key] = void 0; + } + } + if (isStateful) { + instance.props = isSSR ? props : /* @__PURE__ */ shallowReactive(props); + } else { + if (!instance.type.props) { + instance.props = attrs; + } else { + instance.props = props; + } + } + instance.attrs = attrs; +} +function updateProps(instance, rawProps, rawPrevProps, optimized) { + const { + props, + attrs, + vnode: { patchFlag } + } = instance; + const rawCurrentProps = /* @__PURE__ */ toRaw(props); + const [options] = instance.propsOptions; + let hasAttrsChanged = false; + if ( + // always force full diff in dev + // - #1942 if hmr is enabled with sfc component + // - vite#872 non-sfc component used by sfc component + (optimized || patchFlag > 0) && !(patchFlag & 16) + ) { + if (patchFlag & 8) { + const propsToUpdate = instance.vnode.dynamicProps; + for (let i = 0; i < propsToUpdate.length; i++) { + let key = propsToUpdate[i]; + if (isEmitListener(instance.emitsOptions, key)) { + continue; + } + const value = rawProps[key]; + if (options) { + if (hasOwn(attrs, key)) { + if (value !== attrs[key]) { + attrs[key] = value; + hasAttrsChanged = true; + } + } else { + const camelizedKey = camelize(key); + props[camelizedKey] = resolvePropValue( + options, + rawCurrentProps, + camelizedKey, + value, + instance, + false + ); + } + } else { + if (value !== attrs[key]) { + attrs[key] = value; + hasAttrsChanged = true; + } + } + } + } + } else { + if (setFullProps(instance, rawProps, props, attrs)) { + hasAttrsChanged = true; + } + let kebabKey; + for (const key in rawCurrentProps) { + if (!rawProps || // for camelCase + !hasOwn(rawProps, key) && // it's possible the original props was passed in as kebab-case + // and converted to camelCase (#955) + ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey))) { + if (options) { + if (rawPrevProps && // for camelCase + (rawPrevProps[key] !== void 0 || // for kebab-case + rawPrevProps[kebabKey] !== void 0)) { + props[key] = resolvePropValue( + options, + rawCurrentProps, + key, + void 0, + instance, + true + ); + } + } else { + delete props[key]; + } + } + } + if (attrs !== rawCurrentProps) { + for (const key in attrs) { + if (!rawProps || !hasOwn(rawProps, key) && true) { + delete attrs[key]; + hasAttrsChanged = true; + } + } + } + } + if (hasAttrsChanged) { + trigger(instance.attrs, "set", ""); + } +} +function setFullProps(instance, rawProps, props, attrs) { + const [options, needCastKeys] = instance.propsOptions; + let hasAttrsChanged = false; + let rawCastValues; + if (rawProps) { + for (let key in rawProps) { + if (isReservedProp(key)) { + continue; + } + const value = rawProps[key]; + let camelKey; + if (options && hasOwn(options, camelKey = camelize(key))) { + if (!needCastKeys || !needCastKeys.includes(camelKey)) { + props[camelKey] = value; + } else { + (rawCastValues || (rawCastValues = {}))[camelKey] = value; + } + } else if (!isEmitListener(instance.emitsOptions, key)) { + if (!(key in attrs) || value !== attrs[key]) { + attrs[key] = value; + hasAttrsChanged = true; + } + } + } + } + if (needCastKeys) { + const rawCurrentProps = /* @__PURE__ */ toRaw(props); + const castValues = rawCastValues || EMPTY_OBJ; + for (let i = 0; i < needCastKeys.length; i++) { + const key = needCastKeys[i]; + props[key] = resolvePropValue( + options, + rawCurrentProps, + key, + castValues[key], + instance, + !hasOwn(castValues, key) + ); + } + } + return hasAttrsChanged; +} +function resolvePropValue(options, props, key, value, instance, isAbsent) { + const opt = options[key]; + if (opt != null) { + const hasDefault = hasOwn(opt, "default"); + if (hasDefault && value === void 0) { + const defaultValue = opt.default; + if (opt.type !== Function && !opt.skipFactory && isFunction(defaultValue)) { + const { propsDefaults } = instance; + if (key in propsDefaults) { + value = propsDefaults[key]; + } else { + const reset = setCurrentInstance(instance); + value = propsDefaults[key] = defaultValue.call( + null, + props + ); + reset(); + } + } else { + value = defaultValue; + } + if (instance.ce) { + instance.ce._setProp(key, value); + } + } + if (opt[ + 0 + /* shouldCast */ + ]) { + if (isAbsent && !hasDefault) { + value = false; + } else if (opt[ + 1 + /* shouldCastTrue */ + ] && (value === "" || value === hyphenate(key))) { + value = true; + } + } + } + return value; +} +const mixinPropsCache = /* @__PURE__ */ new WeakMap(); +function normalizePropsOptions(comp, appContext, asMixin = false) { + const cache = asMixin ? mixinPropsCache : appContext.propsCache; + const cached = cache.get(comp); + if (cached) { + return cached; + } + const raw = comp.props; + const normalized = {}; + const needCastKeys = []; + let hasExtends = false; + if (!isFunction(comp)) { + const extendProps = (raw2) => { + hasExtends = true; + const [props, keys] = normalizePropsOptions(raw2, appContext, true); + extend(normalized, props); + if (keys) needCastKeys.push(...keys); + }; + if (!asMixin && appContext.mixins.length) { + appContext.mixins.forEach(extendProps); + } + if (comp.extends) { + extendProps(comp.extends); + } + if (comp.mixins) { + comp.mixins.forEach(extendProps); + } + } + if (!raw && !hasExtends) { + if (isObject$1(comp)) { + cache.set(comp, EMPTY_ARR); + } + return EMPTY_ARR; + } + if (isArray(raw)) { + for (let i = 0; i < raw.length; i++) { + const normalizedKey = camelize(raw[i]); + if (validatePropName(normalizedKey)) { + normalized[normalizedKey] = EMPTY_OBJ; + } + } + } else if (raw) { + for (const key in raw) { + const normalizedKey = camelize(key); + if (validatePropName(normalizedKey)) { + const opt = raw[key]; + const prop = normalized[normalizedKey] = isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt); + const propType = prop.type; + let shouldCast = false; + let shouldCastTrue = true; + if (isArray(propType)) { + for (let index = 0; index < propType.length; ++index) { + const type = propType[index]; + const typeName = isFunction(type) && type.name; + if (typeName === "Boolean") { + shouldCast = true; + break; + } else if (typeName === "String") { + shouldCastTrue = false; + } + } + } else { + shouldCast = isFunction(propType) && propType.name === "Boolean"; + } + prop[ + 0 + /* shouldCast */ + ] = shouldCast; + prop[ + 1 + /* shouldCastTrue */ + ] = shouldCastTrue; + if (shouldCast || hasOwn(prop, "default")) { + needCastKeys.push(normalizedKey); + } + } + } + } + const res = [normalized, needCastKeys]; + if (isObject$1(comp)) { + cache.set(comp, res); + } + return res; +} +function validatePropName(key) { + if (key[0] !== "$" && !isReservedProp(key)) { + return true; + } + return false; +} +const isInternalKey = (key) => key === "_" || key === "_ctx" || key === "$stable"; +const normalizeSlotValue = (value) => isArray(value) ? value.map(normalizeVNode) : [normalizeVNode(value)]; +const normalizeSlot = (key, rawSlot, ctx) => { + if (rawSlot._n) { + return rawSlot; + } + const normalized = withCtx((...args) => { + if (false) ; + return normalizeSlotValue(rawSlot(...args)); + }, ctx); + normalized._c = false; + return normalized; +}; +const normalizeObjectSlots = (rawSlots, slots, instance) => { + const ctx = rawSlots._ctx; + for (const key in rawSlots) { + if (isInternalKey(key)) continue; + const value = rawSlots[key]; + if (isFunction(value)) { + slots[key] = normalizeSlot(key, value, ctx); + } else if (value != null) { + const normalized = normalizeSlotValue(value); + slots[key] = () => normalized; + } + } +}; +const normalizeVNodeSlots = (instance, children) => { + const normalized = normalizeSlotValue(children); + instance.slots.default = () => normalized; +}; +const assignSlots = (slots, children, optimized) => { + for (const key in children) { + if (optimized || !isInternalKey(key)) { + slots[key] = children[key]; + } + } +}; +const initSlots = (instance, children, optimized) => { + const slots = instance.slots = createInternalObject(); + if (instance.vnode.shapeFlag & 32) { + const type = children._; + if (type) { + assignSlots(slots, children, optimized); + if (optimized) { + def(slots, "_", type, true); + } + } else { + normalizeObjectSlots(children, slots); + } + } else if (children) { + normalizeVNodeSlots(instance, children); + } +}; +const updateSlots = (instance, children, optimized) => { + const { vnode, slots } = instance; + let needDeletionCheck = true; + let deletionComparisonTarget = EMPTY_OBJ; + if (vnode.shapeFlag & 32) { + const type = children._; + if (type) { + if (optimized && type === 1) { + needDeletionCheck = false; + } else { + assignSlots(slots, children, optimized); + } + } else { + needDeletionCheck = !children.$stable; + normalizeObjectSlots(children, slots); + } + deletionComparisonTarget = children; + } else if (children) { + normalizeVNodeSlots(instance, children); + deletionComparisonTarget = { default: 1 }; + } + if (needDeletionCheck) { + for (const key in slots) { + if (!isInternalKey(key) && deletionComparisonTarget[key] == null) { + delete slots[key]; + } + } + } +}; +const queuePostRenderEffect = queueEffectWithSuspense; +function createRenderer(options) { + return baseCreateRenderer(options); +} +function createHydrationRenderer(options) { + return baseCreateRenderer(options, createHydrationFunctions); +} +function baseCreateRenderer(options, createHydrationFns) { + const target = getGlobalThis(); + target.__VUE__ = true; + const { + insert: hostInsert, + remove: hostRemove, + patchProp: hostPatchProp, + createElement: hostCreateElement, + createText: hostCreateText, + createComment: hostCreateComment, + setText: hostSetText, + setElementText: hostSetElementText, + parentNode: hostParentNode, + nextSibling: hostNextSibling, + setScopeId: hostSetScopeId = NOOP, + insertStaticContent: hostInsertStaticContent + } = options; + const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, namespace = void 0, slotScopeIds = null, optimized = !!n2.dynamicChildren) => { + if (n1 === n2) { + return; + } + if (n1 && !isSameVNodeType(n1, n2)) { + anchor = getNextHostNode(n1); + unmount(n1, parentComponent, parentSuspense, true); + n1 = null; + } + if (n2.patchFlag === -2) { + optimized = false; + n2.dynamicChildren = null; + } + const { type, ref: ref3, shapeFlag } = n2; + switch (type) { + case Text: + processText(n1, n2, container, anchor); + break; + case Comment: + processCommentNode(n1, n2, container, anchor); + break; + case Static: + if (n1 == null) { + mountStaticNode(n2, container, anchor, namespace); + } + break; + case Fragment: + processFragment( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + break; + default: + if (shapeFlag & 1) { + processElement( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } else if (shapeFlag & 6) { + processComponent( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } else if (shapeFlag & 64) { + type.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + } else if (shapeFlag & 128) { + type.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + } else ; + } + if (ref3 != null && parentComponent) { + setRef(ref3, n1 && n1.ref, parentSuspense, n2 || n1, !n2); + } else if (ref3 == null && n1 && n1.ref != null) { + setRef(n1.ref, null, parentSuspense, n1, true); + } + }; + const processText = (n1, n2, container, anchor) => { + if (n1 == null) { + hostInsert( + n2.el = hostCreateText(n2.children), + container, + anchor + ); + } else { + const el = n2.el = n1.el; + if (n2.children !== n1.children) { + hostSetText(el, n2.children); + } + } + }; + const processCommentNode = (n1, n2, container, anchor) => { + if (n1 == null) { + hostInsert( + n2.el = hostCreateComment(n2.children || ""), + container, + anchor + ); + } else { + n2.el = n1.el; + } + }; + const mountStaticNode = (n2, container, anchor, namespace) => { + [n2.el, n2.anchor] = hostInsertStaticContent( + n2.children, + container, + anchor, + namespace, + n2.el, + n2.anchor + ); + }; + const moveStaticNode = ({ el, anchor }, container, nextSibling) => { + let next; + while (el && el !== anchor) { + next = hostNextSibling(el); + hostInsert(el, container, nextSibling); + el = next; + } + hostInsert(anchor, container, nextSibling); + }; + const removeStaticNode = ({ el, anchor }) => { + let next; + while (el && el !== anchor) { + next = hostNextSibling(el); + hostRemove(el); + el = next; + } + hostRemove(anchor); + }; + const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => { + if (n2.type === "svg") { + namespace = "svg"; + } else if (n2.type === "math") { + namespace = "mathml"; + } + if (n1 == null) { + mountElement( + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } else { + const customElement = n1.el && n1.el._isVueCE ? n1.el : null; + try { + if (customElement) { + customElement._beginPatch(); + } + patchElement( + n1, + n2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } finally { + if (customElement) { + customElement._endPatch(); + } + } + } + }; + const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => { + let el; + let vnodeHook; + const { props, shapeFlag, transition, dirs } = vnode; + el = vnode.el = hostCreateElement( + vnode.type, + namespace, + props && props.is, + props + ); + if (shapeFlag & 8) { + hostSetElementText(el, vnode.children); + } else if (shapeFlag & 16) { + mountChildren( + vnode.children, + el, + null, + parentComponent, + parentSuspense, + resolveChildrenNamespace(vnode, namespace), + slotScopeIds, + optimized + ); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent); + if (props) { + for (const key in props) { + if (key !== "value" && !isReservedProp(key)) { + hostPatchProp(el, key, null, props[key], namespace, parentComponent); + } + } + if ("value" in props) { + hostPatchProp(el, "value", null, props.value, namespace); + } + if (vnodeHook = props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHook, parentComponent, vnode); + } + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + const needCallTransitionHooks = needTransition(parentSuspense, transition); + if (needCallTransitionHooks) { + transition.beforeEnter(el); + } + hostInsert(el, container, anchor); + if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) { + queuePostRenderEffect(() => { + try { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + } finally { + } + }, parentSuspense); + } + }; + const setScopeId = (el, vnode, scopeId, slotScopeIds, parentComponent) => { + if (scopeId) { + hostSetScopeId(el, scopeId); + } + if (slotScopeIds) { + for (let i = 0; i < slotScopeIds.length; i++) { + hostSetScopeId(el, slotScopeIds[i]); + } + } + if (parentComponent) { + let subTree = parentComponent.subTree; + if (vnode === subTree || isSuspense(subTree.type) && (subTree.ssContent === vnode || subTree.ssFallback === vnode)) { + const parentVNode = parentComponent.vnode; + setScopeId( + el, + parentVNode, + parentVNode.scopeId, + parentVNode.slotScopeIds, + parentComponent.parent + ); + } + } + }; + const mountChildren = (children, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, start = 0) => { + for (let i = start; i < children.length; i++) { + const child = children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]); + patch( + null, + child, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const patchElement = (n1, n2, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => { + const el = n2.el = n1.el; + let { patchFlag, dynamicChildren, dirs } = n2; + patchFlag |= n1.patchFlag & 16; + const oldProps = n1.props || EMPTY_OBJ; + const newProps = n2.props || EMPTY_OBJ; + let vnodeHook; + parentComponent && toggleRecurse(parentComponent, false); + if (vnodeHook = newProps.onVnodeBeforeUpdate) { + invokeVNodeHook(vnodeHook, parentComponent, n2, n1); + } + if (dirs) { + invokeDirectiveHook(n2, n1, parentComponent, "beforeUpdate"); + } + parentComponent && toggleRecurse(parentComponent, true); + if ( + // #6385 the old vnode may be a user-wrapped non-isomorphic block + // Force full diff when block metadata is unstable. + dynamicChildren && (!n1.dynamicChildren || n1.dynamicChildren.length !== dynamicChildren.length) + ) { + patchFlag = 0; + optimized = false; + dynamicChildren = null; + } + if (oldProps.innerHTML && newProps.innerHTML == null || oldProps.textContent && newProps.textContent == null) { + hostSetElementText(el, ""); + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + el, + parentComponent, + parentSuspense, + resolveChildrenNamespace(n2, namespace), + slotScopeIds + ); + } else if (!optimized) { + patchChildren( + n1, + n2, + el, + null, + parentComponent, + parentSuspense, + resolveChildrenNamespace(n2, namespace), + slotScopeIds, + false + ); + } + if (patchFlag > 0) { + if (patchFlag & 16) { + patchProps(el, oldProps, newProps, parentComponent, namespace); + } else { + if (patchFlag & 2) { + if (oldProps.class !== newProps.class) { + hostPatchProp(el, "class", null, newProps.class, namespace); + } + } + if (patchFlag & 4) { + hostPatchProp(el, "style", oldProps.style, newProps.style, namespace); + } + if (patchFlag & 8) { + const propsToUpdate = n2.dynamicProps; + for (let i = 0; i < propsToUpdate.length; i++) { + const key = propsToUpdate[i]; + const prev = oldProps[key]; + const next = newProps[key]; + if (next !== prev || key === "value") { + hostPatchProp(el, key, prev, next, namespace, parentComponent); + } + } + } + } + if (patchFlag & 1) { + if (n1.children !== n2.children) { + hostSetElementText(el, n2.children); + } + } + } else if (!optimized && dynamicChildren == null) { + patchProps(el, oldProps, newProps, parentComponent, namespace); + } + if ((vnodeHook = newProps.onVnodeUpdated) || dirs) { + queuePostRenderEffect(() => { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1); + dirs && invokeDirectiveHook(n2, n1, parentComponent, "updated"); + }, parentSuspense); + } + }; + const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, namespace, slotScopeIds) => { + for (let i = 0; i < newChildren.length; i++) { + const oldVNode = oldChildren[i]; + const newVNode = newChildren[i]; + const container = ( + // oldVNode may be an errored async setup() component inside Suspense + // which will not have a mounted element + oldVNode.el && // - In the case of a Fragment, we need to provide the actual parent + // of the Fragment itself so it can move its children. + (oldVNode.type === Fragment || // - In the case of different nodes, there is going to be a replacement + // which also requires the correct parent container + !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. + oldVNode.shapeFlag & (6 | 64 | 128)) ? hostParentNode(oldVNode.el) : ( + // In other cases, the parent container is not actually used so we + // just pass the block element here to avoid a DOM parentNode call. + fallbackContainer + ) + ); + patch( + oldVNode, + newVNode, + container, + null, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + true + ); + } + }; + const patchProps = (el, oldProps, newProps, parentComponent, namespace) => { + if (oldProps !== newProps) { + if (oldProps !== EMPTY_OBJ) { + for (const key in oldProps) { + if (!isReservedProp(key) && !(key in newProps)) { + hostPatchProp( + el, + key, + oldProps[key], + null, + namespace, + parentComponent + ); + } + } + } + for (const key in newProps) { + if (isReservedProp(key)) continue; + const next = newProps[key]; + const prev = oldProps[key]; + if (next !== prev && key !== "value") { + hostPatchProp(el, key, prev, next, namespace, parentComponent); + } + } + if ("value" in newProps) { + hostPatchProp(el, "value", oldProps.value, newProps.value, namespace); + } + } + }; + const processFragment = (n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => { + const fragmentStartAnchor = n2.el = n1 ? n1.el : hostCreateText(""); + const fragmentEndAnchor = n2.anchor = n1 ? n1.anchor : hostCreateText(""); + let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + if (n1 == null) { + hostInsert(fragmentStartAnchor, container, anchor); + hostInsert(fragmentEndAnchor, container, anchor); + mountChildren( + // #10007 + // such fragment like `<>` will be compiled into + // a fragment which doesn't have a children. + // In this case fallback to an empty array + n2.children || [], + container, + fragmentEndAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } else { + if (patchFlag > 0 && patchFlag & 64 && dynamicChildren && // #2715 the previous fragment could've been a BAILed one as a result + // of renderSlot() with no valid children + n1.dynamicChildren && n1.dynamicChildren.length === dynamicChildren.length) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + container, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + if ( + // #2080 if the stable fragment has a key, it's a