diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 65a94f1..6914981 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,8 +5,8 @@ "email": "daymadev89@gmail.com" }, "metadata": { - "description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, documentation cleanup, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, and CCPM skill registry search and management", - "version": "1.16.0", + "description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, documentation cleanup, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, and iOS app development with XcodeGen and SwiftUI", + "version": "1.18.0", "homepage": "https://github.com/daymade/claude-code-skills" }, "plugins": [ @@ -15,7 +15,7 @@ "description": "Essential meta-skill for creating effective Claude Code skills with initialization scripts, validation, packaging, marketplace registration, and privacy best practices", "source": "./", "strict": false, - "version": "1.2.1", + "version": "1.2.2", "category": "developer-tools", "keywords": ["skill-creation", "claude-code", "development", "tooling", "workflow", "meta-skill", "essential"], "skills": ["./skill-creator"] @@ -239,6 +239,26 @@ "category": "developer-tools", "keywords": ["ccpm", "skills", "search", "install", "registry", "plugin", "marketplace", "discovery", "skill-management"], "skills": ["./skills-search"] + }, + { + "name": "promptfoo-evaluation", + "description": "Configures and runs LLM evaluation using Promptfoo framework. Use when setting up prompt testing, creating evaluation configs (promptfooconfig.yaml), writing Python custom assertions, implementing llm-rubric for LLM-as-judge, or managing few-shot examples in prompts. Triggers on keywords like promptfoo, eval, LLM evaluation, prompt testing, or model comparison", + "source": "./", + "strict": false, + "version": "1.0.0", + "category": "developer-tools", + "keywords": ["promptfoo", "evaluation", "llm-testing", "prompt-testing", "assertions", "llm-rubric", "few-shot", "model-comparison"], + "skills": ["./promptfoo-evaluation"] + }, + { + "name": "iOS-APP-developer", + "description": "Develops iOS applications with XcodeGen, SwiftUI, and SPM. Use when configuring XcodeGen project.yml, resolving SPM dependency issues, deploying to devices, handling code signing, debugging camera/AVFoundation, iOS version compatibility issues, or fixing Library not loaded @rpath framework errors. Includes state machine testing patterns for @MainActor classes", + "source": "./", + "strict": false, + "version": "1.1.0", + "category": "developer-tools", + "keywords": ["ios", "xcodegen", "swiftui", "spm", "avfoundation", "camera", "code-signing", "device-deployment", "swift", "xcode", "testing"], + "skills": ["./iOS-APP-developer"] } ] } diff --git a/iOS-APP-developer/.security-scan-passed b/iOS-APP-developer/.security-scan-passed new file mode 100644 index 0000000..a8e3a37 --- /dev/null +++ b/iOS-APP-developer/.security-scan-passed @@ -0,0 +1,4 @@ +Security scan passed +Scanned at: 2025-12-15T22:23:52.306779 +Tool: gitleaks + pattern-based validation +Content hash: 1e3661252b87a1effd6b4e458364ab4e7b8bd6356360aea058593e70ed0edd11 diff --git a/iOS-APP-developer/SKILL.md b/iOS-APP-developer/SKILL.md new file mode 100644 index 0000000..788ccb7 --- /dev/null +++ b/iOS-APP-developer/SKILL.md @@ -0,0 +1,305 @@ +--- +name: developing-ios-apps +description: Develops iOS applications with XcodeGen, SwiftUI, and SPM. Triggers on XcodeGen project.yml configuration, SPM dependency issues, device deployment problems, code signing errors, camera/AVFoundation debugging, iOS version compatibility, or "Library not loaded @rpath" framework errors. Use when building iOS apps, fixing Xcode build failures, or deploying to real devices. +--- + +# iOS App Development + +Build, configure, and deploy iOS applications using XcodeGen and Swift Package Manager. + +## Critical Warnings + +| Issue | Cause | Solution | +|-------|-------|----------| +| "Library not loaded: @rpath/Framework" | XcodeGen doesn't auto-embed SPM dynamic frameworks | **Build in Xcode GUI first** (not xcodebuild). See [Troubleshooting](#spm-dynamic-framework-not-embedded) | +| `xcodegen generate` loses signing | Overwrites project settings | Configure in `project.yml` target settings, not global | +| Command-line signing fails | Free Apple ID limitation | Use Xcode GUI or paid developer account ($99/yr) | +| "Cannot be set when automaticallyAdjustsVideoMirroring is YES" | Setting `isVideoMirrored` without disabling automatic | Set `automaticallyAdjustsVideoMirroring = false` first. See [Camera](#camera--avfoundation) | + +## Quick Reference + +| Task | Command | +|------|---------| +| Generate project | `xcodegen generate` | +| Build simulator | `xcodebuild -destination 'platform=iOS Simulator,name=iPhone 17' build` | +| Build device (paid account) | `xcodebuild -destination 'platform=iOS,name=DEVICE' -allowProvisioningUpdates build` | +| Clean DerivedData | `rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*` | +| Find device name | `xcrun xctrace list devices` | + +## XcodeGen Configuration + +### Minimal project.yml + +```yaml +name: AppName +options: + bundleIdPrefix: com.company + deploymentTarget: + iOS: "16.0" + +settings: + base: + SWIFT_VERSION: "6.0" + +packages: + SomePackage: + url: https://github.com/org/repo + from: "1.0.0" + +targets: + AppName: + type: application + platform: iOS + sources: + - path: AppName + settings: + base: + INFOPLIST_FILE: AppName/Info.plist + PRODUCT_BUNDLE_IDENTIFIER: com.company.appname + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: TEAM_ID_HERE + dependencies: + - package: SomePackage +``` + +### Code Signing Configuration + +**Personal (free) account**: Works in Xcode GUI only. Command-line builds require paid account. + +```yaml +# In target settings +settings: + base: + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: TEAM_ID # Get from Xcode → Settings → Accounts +``` + +**Get Team ID**: +```bash +security find-identity -v -p codesigning | head -3 +``` + +## iOS Version Compatibility + +### API Changes by Version + +| iOS 17+ Only | iOS 16 Compatible | +|--------------|-------------------| +| `.onChange { old, new in }` | `.onChange { new in }` | +| `ContentUnavailableView` | Custom VStack | +| `AVAudioApplication` | `AVAudioSession` | +| `@Observable` macro | `@ObservableObject` | +| SwiftData | CoreData/Realm | + +### Lowering Deployment Target + +1. Update `project.yml`: +```yaml +deploymentTarget: + iOS: "16.0" +``` + +2. Fix incompatible APIs: +```swift +// iOS 17 +.onChange(of: value) { oldValue, newValue in } +// iOS 16 +.onChange(of: value) { newValue in } + +// iOS 17 +ContentUnavailableView("Title", systemImage: "icon") +// iOS 16 +VStack { + Image(systemName: "icon").font(.system(size: 48)) + Text("Title").font(.title2.bold()) +} + +// iOS 17 +AVAudioApplication.shared.recordPermission +// iOS 16 +AVAudioSession.sharedInstance().recordPermission +``` + +3. Regenerate: `xcodegen generate` + +## Device Deployment + +### First-time Setup + +1. Connect device via USB +2. Trust computer on device +3. In Xcode: Settings → Accounts → Add Apple ID +4. Select device in scheme dropdown +5. Run (`Cmd + R`) +6. On device: Settings → General → VPN & Device Management → Trust + +### Command-line Build (requires paid account) + +```bash +xcodebuild \ + -project App.xcodeproj \ + -scheme App \ + -destination 'platform=iOS,name=DeviceName' \ + -allowProvisioningUpdates \ + build +``` + +### Common Issues + +| Error | Solution | +|-------|----------| +| "Library not loaded: @rpath/Framework" | SPM dynamic framework not embedded. Build in Xcode GUI first, then CLI works | +| "No Account for Team" | Add Apple ID in Xcode Settings → Accounts | +| "Provisioning profile not found" | Free account limitation. Use Xcode GUI or get paid account | +| Device not listed | Reconnect USB, trust computer on device, restart Xcode | +| DerivedData won't delete | Close Xcode first: `pkill -9 Xcode && rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*` | + +### Free vs Paid Developer Account + +| Feature | Free Apple ID | Paid ($99/year) | +|---------|---------------|-----------------| +| Xcode GUI builds | ✅ | ✅ | +| Command-line builds | ❌ | ✅ | +| App validity | 7 days | 1 year | +| App Store | ❌ | ✅ | +| CI/CD | ❌ | ✅ | + +## SPM Dependencies + +### SPM Dynamic Framework Not Embedded + +**Root Cause**: XcodeGen doesn't generate the "Embed Frameworks" build phase for SPM dynamic frameworks (like RealmSwift, Realm). The app builds successfully but crashes on launch with: + +``` +dyld: Library not loaded: @rpath/RealmSwift.framework/RealmSwift + Referenced from: /var/containers/Bundle/Application/.../App.app/App + Reason: image not found +``` + +**Why This Happens**: +- Static frameworks (most SPM packages) are linked into the binary - no embedding needed +- Dynamic frameworks (RealmSwift, etc.) must be copied into the app bundle +- XcodeGen generates link phase but NOT embed phase for SPM packages +- `embed: true` in project.yml causes build errors (XcodeGen limitation) + +**The Fix** (Manual, one-time per project): +1. Open project in Xcode GUI +2. Select target → General → Frameworks, Libraries +3. Find the dynamic framework (RealmSwift) +4. Change "Do Not Embed" → "Embed & Sign" +5. Build and run from Xcode GUI first + +**After Manual Fix**: Command-line builds (`xcodebuild`) will work because Xcode persists the embed setting in project.pbxproj. + +**Identifying Dynamic Frameworks**: +```bash +# Check if a framework is dynamic +file ~/Library/Developer/Xcode/DerivedData/PROJECT-*/Build/Products/Debug-iphoneos/FRAMEWORK.framework/FRAMEWORK +# Dynamic: "Mach-O 64-bit dynamically linked shared library" +# Static: "current ar archive" +``` + +### Adding Packages + +```yaml +packages: + AudioKit: + url: https://github.com/AudioKit/AudioKit + from: "5.6.5" + RealmSwift: + url: https://github.com/realm/realm-swift + from: "10.54.6" + +targets: + App: + dependencies: + - package: AudioKit + - package: RealmSwift + product: RealmSwift # Explicit product name when package has multiple +``` + +### Resolving Dependencies (China proxy) + +```bash +git config --global http.proxy http://127.0.0.1:1082 +git config --global https.proxy http://127.0.0.1:1082 +xcodebuild -scmProvider system -resolvePackageDependencies +``` + +**Never clear global SPM cache** (`~/Library/Caches/org.swift.swiftpm`). Re-downloading is slow. + +## Camera / AVFoundation + +Camera preview requires real device (simulator has no camera). + +### Quick Debugging Checklist + +1. **Permission**: Added `NSCameraUsageDescription` to Info.plist? +2. **Device**: Running on real device, not simulator? +3. **Session running**: `session.startRunning()` called on background thread? +4. **View size**: UIViewRepresentable has non-zero bounds? +5. **Video mirroring**: Disabled `automaticallyAdjustsVideoMirroring` before setting `isVideoMirrored`? + +### Video Mirroring (Front Camera) + +**CRITICAL**: Must disable automatic adjustment before setting manual mirroring: + +```swift +// WRONG - crashes with "Cannot be set when automaticallyAdjustsVideoMirroring is YES" +connection.isVideoMirrored = true + +// CORRECT - disable automatic first +connection.automaticallyAdjustsVideoMirroring = false +connection.isVideoMirrored = true +``` + +### UIViewRepresentable Sizing Issue + +UIViewRepresentable in ZStack may have zero bounds. Fix with explicit frame: + +```swift +// BAD: UIViewRepresentable may get zero size in ZStack +ZStack { + CameraPreviewView(session: session) // May be invisible! + OtherContent() +} + +// GOOD: Explicit sizing +ZStack { + GeometryReader { geo in + CameraPreviewView(session: session) + .frame(width: geo.size.width, height: geo.size.height) + } + .ignoresSafeArea() + OtherContent() +} +``` + +### Debug Logging Pattern + +Add logging to trace camera flow: + +```swift +import os +private let logger = Logger(subsystem: "com.app", category: "Camera") + +func start() async { + logger.info("start() called, isRunning=\(self.isRunning)") + // ... setup code ... + logger.info("session.startRunning() completed") +} + +// For CGRect (doesn't conform to CustomStringConvertible) +logger.info("bounds=\(NSCoder.string(for: self.bounds))") +``` + +Filter in Console.app by subsystem. + +**For detailed camera implementation**: See [references/camera-avfoundation.md](references/camera-avfoundation.md) + +## Resources + +- [references/xcodegen-full.md](references/xcodegen-full.md) - Complete project.yml options +- [references/swiftui-compatibility.md](references/swiftui-compatibility.md) - iOS version API differences +- [references/camera-avfoundation.md](references/camera-avfoundation.md) - Camera preview debugging +- [references/testing-mainactor.md](references/testing-mainactor.md) - Testing @MainActor classes (state machines, regression tests) diff --git a/iOS-APP-developer/references/camera-avfoundation.md b/iOS-APP-developer/references/camera-avfoundation.md new file mode 100644 index 0000000..754a05a --- /dev/null +++ b/iOS-APP-developer/references/camera-avfoundation.md @@ -0,0 +1,292 @@ +# Camera / AVFoundation Reference + +## Camera Preview Implementation + +### Complete Working Example + +```swift +import SwiftUI +import AVFoundation +import os + +private let logger = Logger(subsystem: "com.app", category: "Camera") + +// MARK: - Session Manager + +@MainActor +final class CameraSessionManager: ObservableObject { + @Published private(set) var isRunning = false + @Published private(set) var error: CameraError? + + let session = AVCaptureSession() + private var videoInput: AVCaptureDeviceInput? + + enum CameraError: LocalizedError { + case noCamera + case setupFailed(String) + case permissionDenied + + var errorDescription: String? { + switch self { + case .noCamera: return "No camera available" + case .setupFailed(let reason): return "Setup failed: \(reason)" + case .permissionDenied: return "Camera permission denied" + } + } + } + + func start() async { + logger.info("start() called, isRunning=\(self.isRunning)") + guard !isRunning else { return } + + // Check permission + guard await requestPermission() else { + error = .permissionDenied + return + } + + // Get camera + guard let device = AVCaptureDevice.default( + .builtInWideAngleCamera, + for: .video, + position: .front + ) else { + logger.error("No front camera available") + error = .noCamera + return + } + + // Configure session + session.beginConfiguration() + session.sessionPreset = .high + + do { + let input = try AVCaptureDeviceInput(device: device) + if session.canAddInput(input) { + session.addInput(input) + videoInput = input + } + session.commitConfiguration() + + // Start on background thread + await withCheckedContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.session.startRunning() + DispatchQueue.main.async { + self?.isRunning = true + logger.info("Camera session started") + continuation.resume() + } + } + } + } catch { + session.commitConfiguration() + self.error = .setupFailed(error.localizedDescription) + } + } + + func stop() { + guard isRunning else { return } + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.session.stopRunning() + DispatchQueue.main.async { + self?.isRunning = false + } + } + } + + private func requestPermission() async -> Bool { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: return true + case .notDetermined: + return await AVCaptureDevice.requestAccess(for: .video) + default: return false + } + } +} + +// MARK: - SwiftUI View + +struct CameraPreviewView: UIViewRepresentable { + let session: AVCaptureSession + + func makeUIView(context: Context) -> CameraPreviewUIView { + let view = CameraPreviewUIView() + view.backgroundColor = .black + view.session = session + return view + } + + func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {} +} + +final class CameraPreviewUIView: UIView { + override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } + + var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } + + var session: AVCaptureSession? { + get { previewLayer.session } + set { + previewLayer.session = newValue + previewLayer.videoGravity = .resizeAspectFill + configureMirroring() + } + } + + private func configureMirroring() { + guard let connection = previewLayer.connection, + connection.isVideoMirroringSupported else { return } + // CRITICAL: Must disable automatic adjustment BEFORE setting manual mirroring + // Without this, iOS throws: "Cannot be set when automaticallyAdjustsVideoMirroring is YES" + connection.automaticallyAdjustsVideoMirroring = false + connection.isVideoMirrored = true + } + + override func layoutSubviews() { + super.layoutSubviews() + previewLayer.frame = bounds + } +} + +// MARK: - Usage in SwiftUI + +struct ContentView: View { + @StateObject private var cameraManager = CameraSessionManager() + + var body: some View { + ZStack { + // CRITICAL: Use GeometryReader for proper sizing + GeometryReader { geo in + CameraPreviewView(session: cameraManager.session) + .frame(width: geo.size.width, height: geo.size.height) + } + .ignoresSafeArea() + + // Overlay content here + } + .onAppear { + Task { await cameraManager.start() } + } + .onDisappear { + cameraManager.stop() + } + } +} +``` + +## Common Issues and Solutions + +### Issue: Camera preview shows nothing + +**Debug steps:** + +1. Check if running on simulator (camera not available): +```swift +#if targetEnvironment(simulator) +logger.warning("Camera not available on simulator") +#endif +``` + +2. Add logging to trace execution: +```swift +logger.info("Permission status: \(AVCaptureDevice.authorizationStatus(for: .video).rawValue)") +logger.info("Session running: \(session.isRunning)") +logger.info("Preview layer bounds: \(previewLayer.bounds)") +``` + +3. Verify Info.plist has camera permission: +```xml +NSCameraUsageDescription +Camera access for preview +``` + +### Issue: UIViewRepresentable has zero size + +**Cause**: In ZStack, UIViewRepresentable doesn't expand like SwiftUI views. + +**Solution**: Wrap in GeometryReader with explicit frame: +```swift +GeometryReader { geo in + CameraPreviewView(session: session) + .frame(width: geo.size.width, height: geo.size.height) +} +``` + +### Issue: Preview layer connection is nil + +**Cause**: Connection isn't established until session is running and layer is in view hierarchy. + +**Solution**: Configure mirroring in layoutSubviews: +```swift +override func layoutSubviews() { + super.layoutSubviews() + previewLayer.frame = bounds + // Retry mirroring here + configureMirroring() +} + +private func configureMirroring() { + guard let conn = previewLayer.connection, + conn.isVideoMirroringSupported else { return } + conn.automaticallyAdjustsVideoMirroring = false + conn.isVideoMirrored = true +} +``` + +### Issue: Crash on setVideoMirrored + +**Error**: `*** -[AVCaptureConnection setVideoMirrored:] Cannot be set when automaticallyAdjustsVideoMirroring is YES` + +**Cause**: iOS automatically adjusts mirroring by default. Setting `isVideoMirrored` while automatic adjustment is enabled throws an exception. + +**Solution**: Always disable automatic adjustment first: +```swift +// WRONG - crashes on some devices +connection.isVideoMirrored = true + +// CORRECT - disable automatic first +connection.automaticallyAdjustsVideoMirroring = false +connection.isVideoMirrored = true +``` + +**Affected Devices**: Primarily older devices (iPhone X, etc.) but can affect any device. + +### Issue: Swift 6 concurrency errors with AVCaptureSession + +**Error**: "cannot access property 'session' with non-Sendable type from nonisolated deinit" + +**Solution**: Don't access session in deinit. Use explicit stop() call: +```swift +deinit { + // Don't access session here + // Cleanup handled by stop() call from view +} +``` + +## Debugging with Console.app + +1. Open Console.app +2. Select your device +3. Filter by: + - Subsystem: `com.yourapp` + - Category: `Camera` +4. Look for the log sequence: + ``` + start() called, isRunning=false + Permission granted + Found front camera: Front Camera + Camera session started + ``` + +## Camera + Audio Conflict + +If using AudioKit or AVAudioEngine, camera audio input may conflict. + +**Solution**: Use video-only input, no audio: +```swift +// Only add video input, skip audio +let videoInput = try AVCaptureDeviceInput(device: videoDevice) +session.addInput(videoInput) +// Do NOT add audio input +``` diff --git a/iOS-APP-developer/references/swiftui-compatibility.md b/iOS-APP-developer/references/swiftui-compatibility.md new file mode 100644 index 0000000..4389bd0 --- /dev/null +++ b/iOS-APP-developer/references/swiftui-compatibility.md @@ -0,0 +1,190 @@ +# SwiftUI iOS Version Compatibility + +## iOS 17 vs iOS 16 API Differences + +### View Modifiers + +#### onChange + +```swift +// iOS 17+ (dual parameter) +.onChange(of: value) { oldValue, newValue in + // Can compare old and new +} + +// iOS 16 (single parameter) +.onChange(of: value) { newValue in + // Only new value available +} +``` + +#### sensoryFeedback (iOS 17+) + +```swift +// iOS 17+ +.sensoryFeedback(.impact, trigger: triggerValue) + +// iOS 16 fallback +UIImpactFeedbackGenerator(style: .medium).impactOccurred() +``` + +### Views + +#### ContentUnavailableView (iOS 17+) + +```swift +// iOS 17+ +ContentUnavailableView( + "No Results", + systemImage: "magnifyingglass", + description: Text("Try a different search") +) + +// iOS 16 fallback +VStack(spacing: 16) { + Image(systemName: "magnifyingglass") + .font(.system(size: 48)) + .foregroundStyle(.secondary) + Text("No Results") + .font(.title2.bold()) + Text("Try a different search") + .font(.subheadline) + .foregroundStyle(.secondary) +} +.frame(maxWidth: .infinity, maxHeight: .infinity) +``` + +#### Inspector (iOS 17+) + +```swift +// iOS 17+ +.inspector(isPresented: $showInspector) { + InspectorContent() +} + +// iOS 16 fallback: Use sheet or sidebar +.sheet(isPresented: $showInspector) { + InspectorContent() +} +``` + +### Observation + +#### @Observable Macro (iOS 17+) + +```swift +// iOS 17+ with @Observable +@Observable +class ViewModel { + var count = 0 +} + +struct ContentView: View { + var viewModel = ViewModel() + var body: some View { + Text("\(viewModel.count)") + } +} + +// iOS 16 with ObservableObject +class ViewModel: ObservableObject { + @Published var count = 0 +} + +struct ContentView: View { + @StateObject var viewModel = ViewModel() + var body: some View { + Text("\(viewModel.count)") + } +} +``` + +### Audio + +#### AVAudioApplication (iOS 17+) + +```swift +// iOS 17+ +let permission = AVAudioApplication.shared.recordPermission +AVAudioApplication.requestRecordPermission { granted in } + +// iOS 16 +let permission = AVAudioSession.sharedInstance().recordPermission +AVAudioSession.sharedInstance().requestRecordPermission { granted in } +``` + +### Animations + +#### Symbol Effects (iOS 17+) + +```swift +// iOS 17+ +Image(systemName: "heart.fill") + .symbolEffect(.bounce, value: isFavorite) + +// iOS 16 fallback +Image(systemName: "heart.fill") + .scaleEffect(isFavorite ? 1.2 : 1.0) + .animation(.spring(), value: isFavorite) +``` + +### Data + +#### SwiftData (iOS 17+) + +```swift +// iOS 17+ with SwiftData +@Model +class Item { + var name: String + var timestamp: Date +} + +// iOS 16: Use CoreData or third-party (Realm) +// CoreData: NSManagedObject subclass +// Realm: Object subclass with @Persisted properties +``` + +## Conditional Compilation + +For features that must use iOS 17 APIs when available: + +```swift +if #available(iOS 17.0, *) { + ContentUnavailableView("Title", systemImage: "icon") +} else { + LegacyEmptyView() +} +``` + +For view modifiers: + +```swift +extension View { + @ViewBuilder + func onChangeCompat(of value: V, perform: @escaping (V) -> Void) -> some View { + if #available(iOS 17.0, *) { + self.onChange(of: value) { _, newValue in + perform(newValue) + } + } else { + self.onChange(of: value, perform: perform) + } + } +} +``` + +## Minimum Deployment Targets by Feature + +| Feature | Minimum iOS | +|---------|-------------| +| SwiftUI basics | 13.0 | +| @StateObject | 14.0 | +| AsyncImage | 15.0 | +| .searchable | 15.0 | +| NavigationStack | 16.0 | +| .navigationDestination | 16.0 | +| @Observable | 17.0 | +| ContentUnavailableView | 17.0 | +| SwiftData | 17.0 | +| .onChange (dual param) | 17.0 | diff --git a/iOS-APP-developer/references/testing-mainactor.md b/iOS-APP-developer/references/testing-mainactor.md new file mode 100644 index 0000000..1740409 --- /dev/null +++ b/iOS-APP-developer/references/testing-mainactor.md @@ -0,0 +1,146 @@ +# Testing @MainActor Classes + +## The Problem + +Testing `@MainActor` classes like `ObservableObject` controllers in Swift 6 is challenging because: + +1. `setUp()` and `tearDown()` are nonisolated +2. Properties with `private(set)` can't be set from tests +3. Direct property access from tests triggers concurrency errors + +## Solution: setStateForTesting Pattern + +Add a DEBUG-only method to allow tests to set internal state: + +```swift +@MainActor +final class TrainingSessionController: ObservableObject { + @Published private(set) var state: TrainingState = .idle + + // ... rest of controller ... + + // MARK: - Testing Support + + #if DEBUG + /// Set state directly for testing purposes only + func setStateForTesting(_ newState: TrainingState) { + state = newState + } + #endif +} +``` + +## Test Class Structure + +```swift +import XCTest +@testable import YourApp + +@MainActor +final class TrainingSessionControllerTests: XCTestCase { + + var controller: TrainingSessionController! + + override func setUp() { + super.setUp() + controller = TrainingSessionController() + } + + override func tearDown() { + controller = nil + super.tearDown() + } + + func testConfigureFromFailedStateAutoResets() { + // Arrange: Set to failed state + controller.setStateForTesting(.failed("Recording too short")) + + // Act: Configure should recover + controller.configure(with: PhaseConfig.default) + + // Assert: Should be back in idle + XCTAssertEqual(controller.state, .idle) + } +} +``` + +## State Machine Testing Patterns + +### Testing State Transitions + +```swift +func testStateTransitions() { + // Test each state's behavior + let states: [TrainingState] = [.idle, .completed, .failed("error")] + + for state in states { + controller.setStateForTesting(state) + controller.configure(with: PhaseConfig.default) + + // Verify expected outcome + XCTAssertTrue(controller.canStart, "\(state) should allow starting") + } +} +``` + +### Regression Tests + +For bugs that have been fixed, add specific regression tests: + +```swift +/// Regression test for: State machine dead-lock after recording failure +/// Bug: After error, controller stayed in failed state forever +func testRegressionFailedStateDeadLock() { + // Simulate the bug scenario + controller.configure(with: PhaseConfig.default) + controller.setStateForTesting(.failed("录音太短")) + + // The fix: configure() should auto-reset from failed state + controller.configure(with: PhaseConfig.default) + + XCTAssertEqual(controller.state, .idle, + "REGRESSION: Failed state should not block configure()") +} +``` + +### State Machine Invariants + +Test invariants that should always hold: + +```swift +/// No terminal state should become a "dead end" +func testAllTerminalStatesAreRecoverable() { + let terminalStates: [TrainingState] = [ + .idle, + .completed, + .failed("test error") + ] + + for state in terminalStates { + controller.setStateForTesting(state) + // Action that should recover + controller.configure(with: PhaseConfig.default) + + // Verify recovery + XCTAssertTrue(canConfigure(), + "\(state) should be recoverable via configure()") + } +} +``` + +## Why This Pattern Works + +1. **`#if DEBUG`**: Method only exists in test builds, zero production overhead +2. **Explicit method**: Makes test-only state manipulation obvious and searchable +3. **MainActor compatible**: Method is part of the @MainActor class +4. **Swift 6 safe**: Avoids concurrency errors by staying on the main actor + +## Alternative: Internal Setter + +If you prefer, you can use `internal(set)` instead of `private(set)`: + +```swift +@Published internal(set) var state: TrainingState = .idle +``` + +However, this is redundant since properties are already internal by default. The `setStateForTesting()` pattern is more explicit about test-only intent. diff --git a/iOS-APP-developer/references/xcodegen-full.md b/iOS-APP-developer/references/xcodegen-full.md new file mode 100644 index 0000000..5d56bee --- /dev/null +++ b/iOS-APP-developer/references/xcodegen-full.md @@ -0,0 +1,196 @@ +# XcodeGen Complete Reference + +## Full project.yml Structure + +```yaml +name: ProjectName + +options: + bundleIdPrefix: com.company + deploymentTarget: + iOS: "16.0" + macOS: "13.0" + xcodeVersion: "16.0" + generateEmptyDirectories: true + createIntermediateGroups: true + +settings: + base: + SWIFT_VERSION: "6.0" + MARKETING_VERSION: "1.0.0" + CURRENT_PROJECT_VERSION: "1" + +packages: + # Basic package + PackageName: + url: https://github.com/org/repo + from: "1.0.0" + + # Exact version + ExactPackage: + url: https://github.com/org/repo + exactVersion: "2.0.0" + + # Branch + BranchPackage: + url: https://github.com/org/repo + branch: main + + # Local package + LocalPackage: + path: ../LocalPackage + +targets: + MainApp: + type: application + platform: iOS + sources: + - path: Sources + excludes: + - "**/.DS_Store" + - "**/Tests/**" + - path: Resources + type: folder + + settings: + base: + INFOPLIST_FILE: Sources/Info.plist + PRODUCT_BUNDLE_IDENTIFIER: com.company.app + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor + LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks" + ENABLE_BITCODE: NO + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: TEAM_ID + configs: + Debug: + SWIFT_OPTIMIZATION_LEVEL: -Onone + Release: + SWIFT_OPTIMIZATION_LEVEL: -O + + dependencies: + # SPM package + - package: PackageName + + # SPM package with explicit product + - package: Firebase + product: FirebaseAnalytics + + # Another target + - target: Framework + + # System framework + - framework: UIKit.framework + + # SDK + - sdk: CoreLocation.framework + + preBuildScripts: + - name: "Run Script" + script: | + echo "Pre-build script" + runOnlyWhenInstalling: false + + postBuildScripts: + - name: "Post Build" + script: | + echo "Post-build script" + + Tests: + type: bundle.unit-test + platform: iOS + sources: + - path: Tests + dependencies: + - target: MainApp + settings: + base: + TEST_HOST: "$(BUILT_PRODUCTS_DIR)/MainApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MainApp" + BUNDLE_LOADER: "$(TEST_HOST)" +``` + +## Target Types + +| Type | Description | +|------|-------------| +| `application` | iOS/macOS app | +| `framework` | Dynamic framework | +| `staticFramework` | Static framework | +| `bundle.unit-test` | Unit test bundle | +| `bundle.ui-testing` | UI test bundle | +| `app-extension` | App extension | +| `watch2-app` | watchOS app | +| `widget-extension` | Widget extension | + +## Build Settings Reference + +### Common Settings + +```yaml +settings: + base: + # Versioning + MARKETING_VERSION: "1.0.0" + CURRENT_PROJECT_VERSION: "1" + + # Swift + SWIFT_VERSION: "6.0" + SWIFT_STRICT_CONCURRENCY: complete + + # Signing + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: TEAM_ID + CODE_SIGN_IDENTITY: "Apple Development" + + # Deployment + IPHONEOS_DEPLOYMENT_TARGET: "16.0" + TARGETED_DEVICE_FAMILY: "1,2" # 1=iPhone, 2=iPad + + # Build + ENABLE_BITCODE: NO + DEBUG_INFORMATION_FORMAT: dwarf-with-dsym + + # Paths + LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks" +``` + +### Per-Configuration Settings + +```yaml +settings: + configs: + Debug: + SWIFT_OPTIMIZATION_LEVEL: -Onone + SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG + MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE + Release: + SWIFT_OPTIMIZATION_LEVEL: -O + SWIFT_COMPILATION_MODE: wholemodule + VALIDATE_PRODUCT: YES +``` + +## Info.plist Keys + +Common keys to add: + +```xml +NSCameraUsageDescription +Camera access description + +NSMicrophoneUsageDescription +Microphone access description + +NSPhotoLibraryUsageDescription +Photo library access description + +UIBackgroundModes + + audio + location + + +UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + +``` diff --git a/promptfoo-evaluation/.security-scan-passed b/promptfoo-evaluation/.security-scan-passed new file mode 100644 index 0000000..6cb5237 --- /dev/null +++ b/promptfoo-evaluation/.security-scan-passed @@ -0,0 +1,4 @@ +Security scan passed +Scanned at: 2025-12-11T22:24:55.327388 +Tool: gitleaks + pattern-based validation +Content hash: d04b93ec8a47fa7b64a2d0ee9790997e5ecc212ddbfa4c2c58fddafa2424d49a diff --git a/promptfoo-evaluation/SKILL.md b/promptfoo-evaluation/SKILL.md new file mode 100644 index 0000000..7af1f11 --- /dev/null +++ b/promptfoo-evaluation/SKILL.md @@ -0,0 +1,392 @@ +--- +name: promptfoo-evaluation +description: Configures and runs LLM evaluation using Promptfoo framework. Use when setting up prompt testing, creating evaluation configs (promptfooconfig.yaml), writing Python custom assertions, implementing llm-rubric for LLM-as-judge, or managing few-shot examples in prompts. Triggers on keywords like "promptfoo", "eval", "LLM evaluation", "prompt testing", or "model comparison". +--- + +# Promptfoo Evaluation + +## Overview + +This skill provides guidance for configuring and running LLM evaluations using [Promptfoo](https://www.promptfoo.dev/), an open-source CLI tool for testing and comparing LLM outputs. + +## Quick Start + +```bash +# Initialize a new evaluation project +npx promptfoo@latest init + +# Run evaluation +npx promptfoo@latest eval + +# View results in browser +npx promptfoo@latest view +``` + +## Configuration Structure + +A typical Promptfoo project structure: + +``` +project/ +├── promptfooconfig.yaml # Main configuration +├── prompts/ +│ ├── system.md # System prompt +│ └── chat.json # Chat format prompt +├── tests/ +│ └── cases.yaml # Test cases +└── scripts/ + └── metrics.py # Custom Python assertions +``` + +## Core Configuration (promptfooconfig.yaml) + +```yaml +# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json +description: "My LLM Evaluation" + +# Prompts to test +prompts: + - file://prompts/system.md + - file://prompts/chat.json + +# Models to compare +providers: + - id: anthropic:messages:claude-sonnet-4-5-20250929 + label: Claude-4.5-Sonnet + - id: openai:gpt-4.1 + label: GPT-4.1 + +# Test cases +tests: file://tests/cases.yaml + +# Default assertions for all tests +defaultTest: + assert: + - type: python + value: file://scripts/metrics.py:custom_assert + - type: llm-rubric + value: | + Evaluate the response quality on a 0-1 scale. + threshold: 0.7 + +# Output path +outputPath: results/eval-results.json +``` + +## Prompt Formats + +### Text Prompt (system.md) + +```markdown +You are a helpful assistant. + +Task: {{task}} +Context: {{context}} +``` + +### Chat Format (chat.json) + +```json +[ + {"role": "system", "content": "{{system_prompt}}"}, + {"role": "user", "content": "{{user_input}}"} +] +``` + +### Few-Shot Pattern + +Embed examples directly in prompt or use chat format with assistant messages: + +```json +[ + {"role": "system", "content": "{{system_prompt}}"}, + {"role": "user", "content": "Example input: {{example_input}}"}, + {"role": "assistant", "content": "{{example_output}}"}, + {"role": "user", "content": "Now process: {{actual_input}}"} +] +``` + +## Test Cases (tests/cases.yaml) + +```yaml +- description: "Test case 1" + vars: + system_prompt: file://prompts/system.md + user_input: "Hello world" + # Load content from files + context: file://data/context.txt + assert: + - type: contains + value: "expected text" + - type: python + value: file://scripts/metrics.py:custom_check + threshold: 0.8 +``` + +## Python Custom Assertions + +Create a Python file for custom assertions (e.g., `scripts/metrics.py`): + +```python +def get_assert(output: str, context: dict) -> dict: + """Default assertion function.""" + vars_dict = context.get('vars', {}) + + # Access test variables + expected = vars_dict.get('expected', '') + + # Return result + return { + "pass": expected in output, + "score": 0.8, + "reason": "Contains expected content", + "named_scores": {"relevance": 0.9} + } + +def custom_check(output: str, context: dict) -> dict: + """Custom named assertion.""" + word_count = len(output.split()) + passed = 100 <= word_count <= 500 + + return { + "pass": passed, + "score": min(1.0, word_count / 300), + "reason": f"Word count: {word_count}" + } +``` + +**Key points:** +- Default function name is `get_assert` +- Specify function with `file://path.py:function_name` +- Return `bool`, `float` (score), or `dict` with pass/score/reason +- Access variables via `context['vars']` + +## LLM-as-Judge (llm-rubric) + +```yaml +assert: + - type: llm-rubric + value: | + Evaluate the response based on: + 1. Accuracy of information + 2. Clarity of explanation + 3. Completeness + + Score 0.0-1.0 where 0.7+ is passing. + threshold: 0.7 + provider: openai:gpt-4.1 # Optional: override grader model +``` + +**Best practices:** +- Provide clear scoring criteria +- Use `threshold` to set minimum passing score +- Default grader uses available API keys (OpenAI → Anthropic → Google) + +## Common Assertion Types + +| Type | Usage | Example | +|------|-------|---------| +| `contains` | Check substring | `value: "hello"` | +| `icontains` | Case-insensitive | `value: "HELLO"` | +| `equals` | Exact match | `value: "42"` | +| `regex` | Pattern match | `value: "\\d{4}"` | +| `python` | Custom logic | `value: file://script.py` | +| `llm-rubric` | LLM grading | `value: "Is professional"` | +| `latency` | Response time | `threshold: 1000` | + +## File References + +All paths are relative to config file location: + +```yaml +# Load file content as variable +vars: + content: file://data/input.txt + +# Load prompt from file +prompts: + - file://prompts/main.md + +# Load test cases from file +tests: file://tests/cases.yaml + +# Load Python assertion +assert: + - type: python + value: file://scripts/check.py:validate +``` + +## Running Evaluations + +```bash +# Basic run +npx promptfoo@latest eval + +# With specific config +npx promptfoo@latest eval --config path/to/config.yaml + +# Output to file +npx promptfoo@latest eval --output results.json + +# Filter tests +npx promptfoo@latest eval --filter-metadata category=math + +# View results +npx promptfoo@latest view +``` + +## Troubleshooting + +**Python not found:** +```bash +export PROMPTFOO_PYTHON=python3 +``` + +**Large outputs truncated:** +Outputs over 30000 characters are truncated. Use `head_limit` in assertions. + +**File not found errors:** +Ensure paths are relative to `promptfooconfig.yaml` location. + +## Echo Provider (Preview Mode) + +Use the **echo provider** to preview rendered prompts without making API calls: + +```yaml +# promptfooconfig-preview.yaml +providers: + - echo # Returns prompt as output, no API calls + +tests: + - vars: + input: "test content" +``` + +**Use cases:** +- Preview prompt rendering before expensive API calls +- Verify Few-shot examples are loaded correctly +- Debug variable substitution issues +- Validate prompt structure + +```bash +# Run preview mode +npx promptfoo@latest eval --config promptfooconfig-preview.yaml +``` + +**Cost:** Free - no API tokens consumed. + +## Advanced Few-Shot Implementation + +### Multi-turn Conversation Pattern + +For complex few-shot learning with full examples: + +```json +[ + {"role": "system", "content": "{{system_prompt}}"}, + + // Few-shot Example 1 + {"role": "user", "content": "Task: {{example_input_1}}"}, + {"role": "assistant", "content": "{{example_output_1}}"}, + + // Few-shot Example 2 (optional) + {"role": "user", "content": "Task: {{example_input_2}}"}, + {"role": "assistant", "content": "{{example_output_2}}"}, + + // Actual test + {"role": "user", "content": "Task: {{actual_input}}"} +] +``` + +**Test case configuration:** + +```yaml +tests: + - vars: + system_prompt: file://prompts/system.md + # Few-shot examples + example_input_1: file://data/examples/input1.txt + example_output_1: file://data/examples/output1.txt + example_input_2: file://data/examples/input2.txt + example_output_2: file://data/examples/output2.txt + # Actual test + actual_input: file://data/test1.txt +``` + +**Best practices:** +- Use 1-3 few-shot examples (more may dilute effectiveness) +- Ensure examples match the task format exactly +- Load examples from files for better maintainability +- Use echo provider first to verify structure + +## Long Text Handling + +For Chinese/long-form content evaluations (10k+ characters): + +**Configuration:** + +```yaml +providers: + - id: anthropic:messages:claude-sonnet-4-5-20250929 + config: + max_tokens: 8192 # Increase for long outputs + +defaultTest: + assert: + - type: python + value: file://scripts/metrics.py:check_length +``` + +**Python assertion for text metrics:** + +```python +import re + +def strip_tags(text: str) -> str: + """Remove HTML tags for pure text.""" + return re.sub(r'<[^>]+>', '', text) + +def check_length(output: str, context: dict) -> dict: + """Check output length constraints.""" + raw_input = context['vars'].get('raw_input', '') + + input_len = len(strip_tags(raw_input)) + output_len = len(strip_tags(output)) + + reduction_ratio = 1 - (output_len / input_len) if input_len > 0 else 0 + + return { + "pass": 0.7 <= reduction_ratio <= 0.9, + "score": reduction_ratio, + "reason": f"Reduction: {reduction_ratio:.1%} (target: 70-90%)", + "named_scores": { + "input_length": input_len, + "output_length": output_len, + "reduction_ratio": reduction_ratio + } + } +``` + +## Real-World Example + +**Project:** Chinese short-video content curation from long transcripts + +**Structure:** +``` +tiaogaoren/ +├── promptfooconfig.yaml # Production config +├── promptfooconfig-preview.yaml # Preview config (echo provider) +├── prompts/ +│ ├── tiaogaoren-prompt.json # Chat format with few-shot +│ └── v4/system-v4.md # System prompt +├── tests/cases.yaml # 3 test samples +├── scripts/metrics.py # Custom metrics (reduction ratio, etc.) +├── data/ # 5 samples (2 few-shot, 3 eval) +└── results/ +``` + +**See:** `~/workspace/prompts/tiaogaoren/` for full implementation. + +## Resources + +For detailed API reference and advanced patterns, see [references/promptfoo_api.md](references/promptfoo_api.md). diff --git a/promptfoo-evaluation/references/promptfoo_api.md b/promptfoo-evaluation/references/promptfoo_api.md new file mode 100644 index 0000000..2c723e4 --- /dev/null +++ b/promptfoo-evaluation/references/promptfoo_api.md @@ -0,0 +1,249 @@ +# Promptfoo API Reference + +## Provider Configuration + +### Echo Provider (No API Calls) + +```yaml +providers: + - echo # Returns prompt as-is, no API calls +``` + +**Use cases:** +- Preview rendered prompts without cost +- Debug variable substitution +- Verify few-shot structure +- Test configuration before production runs + +**Cost:** Free - no tokens consumed. + +### Anthropic + +```yaml +providers: + - id: anthropic:messages:claude-sonnet-4-5-20250929 + config: + max_tokens: 4096 + temperature: 0.7 +``` + +### OpenAI + +```yaml +providers: + - id: openai:gpt-4.1 + config: + temperature: 0.5 + max_tokens: 2048 +``` + +### Multiple Providers (A/B Testing) + +```yaml +providers: + - id: anthropic:messages:claude-sonnet-4-5-20250929 + label: Claude + - id: openai:gpt-4.1 + label: GPT-4.1 +``` + +## Assertion Reference + +### Python Assertion Context + +```python +class AssertionContext: + prompt: str # Raw prompt sent to LLM + vars: dict # Test case variables + test: dict # Complete test case + config: dict # Assertion config + provider: Any # Provider info + providerResponse: Any # Full response +``` + +### GradingResult Format + +```python +{ + "pass": bool, # Required: pass/fail + "score": float, # 0.0-1.0 score + "reason": str, # Explanation + "named_scores": dict, # Custom metrics + "component_results": [] # Nested results +} +``` + +### Assertion Types + +| Type | Description | Parameters | +|------|-------------|------------| +| `contains` | Substring check | `value` | +| `icontains` | Case-insensitive | `value` | +| `equals` | Exact match | `value` | +| `regex` | Pattern match | `value` | +| `not-contains` | Absence check | `value` | +| `starts-with` | Prefix check | `value` | +| `contains-any` | Any substring | `value` (array) | +| `contains-all` | All substrings | `value` (array) | +| `cost` | Token cost | `threshold` | +| `latency` | Response time | `threshold` (ms) | +| `perplexity` | Model confidence | `threshold` | +| `python` | Custom Python | `value` (file/code) | +| `javascript` | Custom JS | `value` (code) | +| `llm-rubric` | LLM grading | `value`, `threshold` | +| `factuality` | Fact checking | `value` (reference) | +| `model-graded-closedqa` | Q&A grading | `value` | +| `similar` | Semantic similarity | `value`, `threshold` | + +## Test Case Configuration + +### Full Test Case Structure + +```yaml +- description: "Test name" + vars: + var1: "value" + var2: file://path.txt + assert: + - type: contains + value: "expected" + metadata: + category: "test-category" + priority: high + options: + provider: specific-provider + transform: "output.trim()" +``` + +### Loading Variables from Files + +```yaml +vars: + # Text file (loaded as string) + content: file://data/input.txt + + # JSON/YAML (parsed to object) + config: file://config.json + + # Python script (executed, returns value) + dynamic: file://scripts/generate.py + + # PDF (text extracted) + document: file://docs/report.pdf + + # Image (base64 encoded) + image: file://images/photo.png +``` + +## Advanced Patterns + +### Dynamic Test Generation (Python) + +```python +# tests/generate.py +def get_tests(): + return [ + { + "vars": {"input": f"test {i}"}, + "assert": [{"type": "contains", "value": str(i)}] + } + for i in range(10) + ] +``` + +```yaml +tests: file://tests/generate.py:get_tests +``` + +### Scenario-based Testing + +```yaml +scenarios: + - config: + - vars: + language: "French" + - vars: + language: "Spanish" + tests: + - vars: + text: "Hello" + assert: + - type: llm-rubric + value: "Translation is accurate" +``` + +### Transform Output + +```yaml +defaultTest: + options: + transform: | + output.replace(/\n/g, ' ').trim() +``` + +### Custom Grading Provider + +```yaml +defaultTest: + options: + provider: openai:gpt-4.1 + assert: + - type: llm-rubric + value: "Evaluate quality" + provider: anthropic:claude-3-haiku # Override for this assertion +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `ANTHROPIC_API_KEY` | Anthropic API key | +| `OPENAI_API_KEY` | OpenAI API key | +| `PROMPTFOO_PYTHON` | Python binary path | +| `PROMPTFOO_CACHE_ENABLED` | Enable caching (default: true) | +| `PROMPTFOO_CACHE_PATH` | Cache directory | + +## CLI Commands + +```bash +# Initialize project +npx promptfoo@latest init + +# Run evaluation +npx promptfoo@latest eval [options] + +# Options: +# --config Config file path +# --output Output file path +# --grader Override grader model +# --no-cache Disable caching +# --filter-metadata Filter tests by metadata +# --repeat Repeat each test n times +# --delay Delay between requests +# --max-concurrency Parallel requests + +# View results +npx promptfoo@latest view [options] + +# Share results +npx promptfoo@latest share + +# Generate report +npx promptfoo@latest generate dataset +``` + +## Output Formats + +```bash +# JSON (default) +--output results.json + +# CSV +--output results.csv + +# HTML report +--output results.html + +# YAML +--output results.yaml +``` diff --git a/skill-creator/SKILL.md b/skill-creator/SKILL.md index e033fdd..8d3231b 100644 --- a/skill-creator/SKILL.md +++ b/skill-creator/SKILL.md @@ -108,7 +108,7 @@ Skills use a three-level loading system to manage context efficiently: ### Skill Creation Best Practice -Anthropic has wrote skill authoring best practices, you SHOULD retrieve it before you create or update any skills, the link is https://docs.claude.com/en/docs/agents-and-tools/agent-skills/best-practices.md +Anthropic has wrote skill authoring best practices, you SHOULD retrieve it before you create or update any skills, the link is https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices.md ## ⚠️ CRITICAL: Edit Skills at Source Location diff --git a/skill-creator/scripts/quick_validate.py b/skill-creator/scripts/quick_validate.py index cc4ebbc..29a452e 100755 --- a/skill-creator/scripts/quick_validate.py +++ b/skill-creator/scripts/quick_validate.py @@ -18,9 +18,11 @@ def find_path_references(content: str) -> list[str]: - Placeholder paths (xxx, example, etc.) - Paths in example contexts (lines containing "Example:", "e.g.", etc.) - Generic documentation examples + - Paths prefixed with file:// (e.g., file://scripts/xxx) - these are external tool references, not skill-internal paths """ # Pattern to match bundled resource paths (scripts/, references/, assets/) - pattern = r'(?:scripts|references|assets)/[\w./-]+' + # Use negative lookbehind to exclude file:// prefixed paths + pattern = r'(?