From 4f07976825a9265b36af992a4666ec1f4463de74 Mon Sep 17 00:00:00 2001 From: daymade Date: Mon, 2 Mar 2026 20:01:18 +0800 Subject: [PATCH] release: prepare v1.37.0 with excel-automation and capture-screen --- .claude-plugin/marketplace.json | 6 +- CHANGELOG.md | 20 ++ CLAUDE.md | 12 +- README.md | 100 +++++- README.zh-CN.md | 98 +++++- capture-screen/.security-scan-passed | 4 + capture-screen/SKILL.md | 238 +++++++++++++ capture-screen/scripts/get_window_id.swift | 57 ++++ excel-automation/.security-scan-passed | 4 + excel-automation/SKILL.md | 313 ++++++++++++++++++ .../references/formatting-reference.md | 122 +++++++ .../scripts/create_formatted_excel.py | 259 +++++++++++++++ .../scripts/parse_complex_excel.py | 278 ++++++++++++++++ promptfoo-evaluation/.security-scan-passed | 4 +- promptfoo-evaluation/SKILL.md | 2 +- promptfoo-evaluation/scripts/metrics.py | 130 ++++++++ 16 files changed, 1629 insertions(+), 18 deletions(-) create mode 100644 capture-screen/.security-scan-passed create mode 100644 capture-screen/SKILL.md create mode 100755 capture-screen/scripts/get_window_id.swift create mode 100644 excel-automation/.security-scan-passed create mode 100644 excel-automation/SKILL.md create mode 100644 excel-automation/references/formatting-reference.md create mode 100755 excel-automation/scripts/create_formatted_excel.py create mode 100755 excel-automation/scripts/parse_complex_excel.py create mode 100755 promptfoo-evaluation/scripts/metrics.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 70dc64a..3c4540a 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, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing, Tailscale+proxy conflict diagnosis with SSH tunnel SOP for remote development, multi-path parallel product analysis with cross-model test-time compute scaling, and real financial data collection for US equities with validation and yfinance pitfall handling", - "version": "1.36.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, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing, Tailscale+proxy conflict diagnosis with SSH tunnel SOP for remote development, multi-path parallel product analysis with cross-model test-time compute scaling, real financial data collection for US equities with validation and yfinance pitfall handling, advanced Excel automation for formatted workbook generation and complex xlsm parsing, and macOS programmatic window screenshot capture workflows", + "version": "1.37.0", "homepage": "https://github.com/daymade/claude-code-skills" }, "plugins": [ @@ -479,7 +479,7 @@ "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", + "version": "1.1.0", "category": "developer-tools", "keywords": [ "promptfoo", diff --git a/CHANGELOG.md b/CHANGELOG.md index c55f150..14c1c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - None +## [1.37.0] - 2026-03-02 + +### Added +- **New Skill**: excel-automation - Create formatted Excel files, parse complex xlsm models, and control Excel on macOS + - Bundled scripts for workbook generation and complex XML/ZIP parsing + - Bundled reference: formatting-reference.md for styles, number formats, and layout patterns + - AppleScript control patterns with timeout-safe execution guidance +- **New Skill**: capture-screen - Programmatic macOS screenshot capture workflows + - Bundled Swift script for CGWindowID discovery + - AppleScript + screencapture multi-shot workflow patterns + - Clear anti-pattern guidance for unreliable window ID methods +- Added missing `promptfoo-evaluation/scripts/metrics.py` referenced by skill examples + +### Changed +- Updated marketplace skills/plugins count from 39 to 41 +- Updated marketplace version from 1.36.0 to 1.37.0 +- Bumped `promptfoo-evaluation` plugin version from 1.0.0 to 1.1.0 (skill content update + missing script fix) +- Updated README.md and README.zh-CN.md badges, installation commands, skill listings, use cases, quick links, and requirements +- Updated CLAUDE.md counts, version reference, and Available Skills list (added #40 and #41) + ## [1.36.0] - 2026-03-02 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 7ba5e42..93c52c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository Overview -This is a Claude Code skills marketplace containing 39 production-ready skills organized in a plugin marketplace structure. Each skill is a self-contained package that extends Claude's capabilities with specialized knowledge, workflows, and bundled resources. +This is a Claude Code skills marketplace containing 41 production-ready skills organized in a plugin marketplace structure. Each skill is a self-contained package that extends Claude's capabilities with specialized knowledge, workflows, and bundled resources. **Essential Skill**: `skill-creator` is the most important skill in this marketplace - it's a meta-skill that enables users to create their own skills. Always recommend it first for users interested in extending Claude Code. @@ -134,7 +134,7 @@ Skills for public distribution must NOT contain: ## Marketplace Configuration The marketplace is configured in `.claude-plugin/marketplace.json`: - - Contains 38 plugins, each mapping to one skill + - Contains 41 plugins, each mapping to one skill - Each plugin has: name, description, version, category, keywords, skills array - Marketplace metadata: name, owner, version, homepage @@ -144,7 +144,7 @@ The marketplace is configured in `.claude-plugin/marketplace.json`: 1. **Marketplace Version** (`.claude-plugin/marketplace.json` → `metadata.version`) - Tracks the marketplace catalog as a whole - - Current: v1.34.1 + - Current: v1.37.0 - Bump when: Adding/removing skills, major marketplace restructuring - Semantic versioning: MAJOR.MINOR.PATCH @@ -213,9 +213,11 @@ This applies when you change ANY file under a skill directory: 34. **deep-research** - Generate format-controlled research reports with evidence mapping, citations, and multi-pass synthesis 35. **competitors-analysis** - Evidence-based competitor tracking and analysis with source citations (file:line_number format) 36. **tunnel-doctor** - Diagnose and fix Tailscale + proxy/VPN conflicts (four layers: route, HTTP env, system proxy, SSH ProxyCommand) on macOS with WSL SSH support - 37. **windows-remote-desktop-connection-doctor** - Diagnose AVD/W365 connection quality issues with transport protocol analysis and Windows App log parsing - 38. **product-analysis** - Perform structured product audits across UX, API, architecture, and compare mode to produce prioritized optimization recommendations + 37. **windows-remote-desktop-connection-doctor** - Diagnose AVD/W365 connection quality issues with transport protocol analysis and Windows App log parsing + 38. **product-analysis** - Perform structured product audits across UX, API, architecture, and compare mode to produce prioritized optimization recommendations 39. **financial-data-collector** - Collect real financial data for US public companies via yfinance with validation, NaN detection, and NO FALLBACK principle + 40. **excel-automation** - Create formatted Excel files, parse complex xlsm models, and control Excel windows on macOS via AppleScript + 41. **capture-screen** - Programmatically capture macOS application windows using Swift window ID discovery and screencapture workflows **Recommendation**: Always suggest `skill-creator` first for users interested in creating skills or extending Claude Code. diff --git a/README.md b/README.md index fb6b5bc..015a5f5 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ [![简体中文](https://img.shields.io/badge/语言-简体中文-red)](./README.zh-CN.md) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Skills](https://img.shields.io/badge/skills-39-blue.svg)](https://github.com/daymade/claude-code-skills) -[![Version](https://img.shields.io/badge/version-1.36.0-green.svg)](https://github.com/daymade/claude-code-skills) +[![Skills](https://img.shields.io/badge/skills-41-blue.svg)](https://github.com/daymade/claude-code-skills) +[![Version](https://img.shields.io/badge/version-1.37.0-green.svg)](https://github.com/daymade/claude-code-skills) [![Claude Code](https://img.shields.io/badge/Claude%20Code-2.0.13+-purple.svg)](https://claude.com/code) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/daymade/claude-code-skills/graphs/commit-activity) -Professional Claude Code skills marketplace featuring 39 production-ready skills for enhanced development workflows. +Professional Claude Code skills marketplace featuring 41 production-ready skills for enhanced development workflows. ## 📑 Table of Contents @@ -231,6 +231,12 @@ claude plugin install product-analysis@daymade-skills # Financial data collection for US equities claude plugin install financial-data-collector@daymade-skills + +# Excel automation for creation, parsing, and macOS control +claude plugin install excel-automation@daymade-skills + +# Programmatic macOS screenshot capture workflows +claude plugin install capture-screen@daymade-skills ``` Each skill can be installed independently - choose only what you need! @@ -1631,7 +1637,7 @@ claude plugin install product-analysis@daymade-skills ### 39. **financial-data-collector** - Financial Data Collection for US Equities -Collect real financial data for any US publicly traded company from free public sources (yfinance). Output structured JSON with market data, historical financials (income statement, cash flow, balance sheet), WACC inputs, and analyst estimates — ready for downstream DCF modeling, comps analysis, or earnings review. +Collect real financial data for any US publicly traded company from free public sources (yfinance). Output structured JSON with market data, historical financials (income statement, cash flow, balance sheet), WACC inputs, and analyst estimates - ready for downstream DCF modeling, comps analysis, or earnings review. **When to use:** - Collecting structured financial data before building DCF or valuation models @@ -1667,6 +1673,82 @@ claude plugin install financial-data-collector@daymade-skills --- +### 40. **excel-automation** - Excel Creation, Parsing, and macOS Control + +Create professionally formatted Excel files, parse complex `.xlsm` models with stdlib XML/ZIP workflows, and control Microsoft Excel windows on macOS via AppleScript. + +**When to use:** +- Building finance-ready spreadsheets with consistent formatting rules +- Parsing complex bank/broker `.xlsm` files that fail in `openpyxl` +- Extracting targeted sheet/cell data without loading huge workbooks +- Automating Excel window operations (zoom, scroll, select) on macOS + +**Key features:** +- Production template for formatted workbook generation via `openpyxl` +- Complex workbook parser using `zipfile` + `xml.etree` (no heavy dependencies) +- Corrupted `definedNames` repair workflow for problematic files +- Verified AppleScript command patterns with timeout safeguards +- Bundled formatting reference for colors, number formats, and table patterns + +**Example usage:** +```bash +# Install the skill +claude plugin install excel-automation@daymade-skills + +# Then ask Claude to automate Excel workflows +"Create a formatted valuation template workbook" +"Parse this .xlsm and extract the DCF sheet" +"Generate an AppleScript sequence to zoom and scroll Excel before screenshot" +``` + +**🎬 Live Demo** + +*Coming soon* + +📚 **Documentation**: See [excel-automation/SKILL.md](./excel-automation/SKILL.md) and [formatting-reference.md](./excel-automation/references/formatting-reference.md). + +**Requirements**: Python 3.8+, `uv`, `openpyxl` (auto via `uv run --with openpyxl`), macOS for AppleScript window control. + +--- + +### 41. **capture-screen** - Programmatic macOS Screenshot Capture + +Capture application windows by CGWindowID with a reliable three-step workflow: discover window IDs via Swift, control app state via AppleScript, and capture outputs with `screencapture`. + +**When to use:** +- Automating repeatable screenshot workflows for documentation +- Capturing specific app windows instead of full-screen screenshots +- Producing multi-shot sequences after scripted scroll/zoom changes +- Building visual evidence capture pipelines on macOS + +**Key features:** +- Bundled Swift script to resolve accurate window IDs (`CGWindowListCopyWindowInfo`) +- Verified AppleScript patterns for app activation and window preparation +- Window-scoped capture commands with silent mode, delays, and format control +- Multi-shot workflow pattern for section-by-section capture +- Clear anti-pattern notes for methods that fail on macOS + +**Example usage:** +```bash +# Install the skill +claude plugin install capture-screen@daymade-skills + +# Then ask Claude to capture windows programmatically +"Find the Excel window ID and capture it silently" +"Create a multi-shot capture workflow for this workbook" +"Capture Chrome window sections with scripted scrolling" +``` + +**🎬 Live Demo** + +*Coming soon* + +📚 **Documentation**: See [capture-screen/SKILL.md](./capture-screen/SKILL.md). + +**Requirements**: macOS (Swift + AppleScript + `screencapture`). + +--- + ## 🎬 Interactive Demo Gallery Want to see all demos in one place with click-to-enlarge functionality? Check out our [interactive demo gallery](./demos/index.html) or browse the [demos directory](./demos/). @@ -1712,6 +1794,12 @@ Use **transcript-fixer** to correct speech-to-text errors in meeting notes, lect ### For Financial Data & Investment Research Use **financial-data-collector** to pull structured financial data for any US public company, then feed the JSON output into DCF modeling, comps analysis, or earnings review workflows. +### For Excel & Financial Modeling Automation +Use **excel-automation** to create formatted workbooks, parse complex `.xlsm` models, and automate Excel window controls for repetitive analyst workflows. + +### For Visual Capture Automation on macOS +Use **capture-screen** to script repeatable app-window screenshots. Combine with **excel-automation** to generate report-ready workbook visuals. + ### For Meeting Documentation Use **meeting-minutes-taker** to transform raw meeting transcripts into structured, evidence-based minutes. Combine with **transcript-fixer** to clean up ASR errors before generating minutes. Features multi-pass generation with UNION merge to avoid content loss. @@ -1809,6 +1897,8 @@ Each skill includes: - **competitors-analysis**: See `competitors-analysis/SKILL.md` for evidence-based analysis workflow and `competitors-analysis/references/profile_template.md` for competitor profile template - **windows-remote-desktop-connection-doctor**: See `windows-remote-desktop-connection-doctor/references/windows_app_log_analysis.md` for log parsing patterns and `windows-remote-desktop-connection-doctor/references/avd_transport_protocols.md` for transport protocol details - **product-analysis**: See `product-analysis/SKILL.md` for workflow and `product-analysis/references/synthesis_methodology.md` for cross-agent weighting and recommendation logic +- **excel-automation**: See `excel-automation/SKILL.md` for create/parse/control workflows and `excel-automation/references/formatting-reference.md` for formatting standards +- **capture-screen**: See `capture-screen/SKILL.md` for CGWindowID-based screenshot workflows on macOS ## 🛠️ Requirements @@ -1832,6 +1922,8 @@ Each skill includes: - **Promptfoo** (for promptfoo-evaluation): `npx promptfoo@latest` - **macOS + Xcode, XcodeGen** (for iOS-APP-developer) - **Codex CLI** (optional, for product-analysis multi-model mode) +- **uv + openpyxl** (for excel-automation): `uv run --with openpyxl ...` +- **macOS** (for capture-screen and excel-automation AppleScript control workflows) ## ❓ FAQ diff --git a/README.zh-CN.md b/README.zh-CN.md index b8090e3..5404eaa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,15 +6,15 @@ [![简体中文](https://img.shields.io/badge/语言-简体中文-red)](./README.zh-CN.md) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Skills](https://img.shields.io/badge/skills-39-blue.svg)](https://github.com/daymade/claude-code-skills) -[![Version](https://img.shields.io/badge/version-1.36.0-green.svg)](https://github.com/daymade/claude-code-skills) +[![Skills](https://img.shields.io/badge/skills-41-blue.svg)](https://github.com/daymade/claude-code-skills) +[![Version](https://img.shields.io/badge/version-1.37.0-green.svg)](https://github.com/daymade/claude-code-skills) [![Claude Code](https://img.shields.io/badge/Claude%20Code-2.0.13+-purple.svg)](https://claude.com/code) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/daymade/claude-code-skills/graphs/commit-activity) -专业的 Claude Code 技能市场,提供 39 个生产就绪的技能,用于增强开发工作流。 +专业的 Claude Code 技能市场,提供 41 个生产就绪的技能,用于增强开发工作流。 ## 📑 目录 @@ -234,6 +234,12 @@ claude plugin install product-analysis@daymade-skills # 美股金融数据采集 claude plugin install financial-data-collector@daymade-skills + +# Excel 创建、解析与 macOS 自动化控制 +claude plugin install excel-automation@daymade-skills + +# macOS 程序化窗口截图工作流 +claude plugin install capture-screen@daymade-skills ``` 每个技能都可以独立安装 - 只选择你需要的! @@ -1709,6 +1715,82 @@ claude plugin install financial-data-collector@daymade-skills --- +### 40. **excel-automation** - Excel 创建、解析与 macOS 控制 + +用于创建专业格式化 Excel、通过标准库 XML/ZIP 解析复杂 `.xlsm` 模型,并在 macOS 上通过 AppleScript 控制 Excel 窗口。 + +**使用场景:** +- 需要按投研规范批量生成格式化工作簿 +- `openpyxl` 无法读取复杂券商/投行 `.xlsm` 模型 +- 需要在不完整加载大文件的情况下抽取目标工作表与单元格 +- 在 macOS 上自动执行 Excel 缩放、滚动、选区等窗口操作 + +**主要功能:** +- 提供可复用的 `openpyxl` 格式化模板脚本 +- 使用 `zipfile` + `xml.etree` 解析复杂工作簿(轻依赖) +- 内置损坏 `definedNames` 修复流程 +- 提供带超时保护的 AppleScript 命令模式 +- 附带格式规范参考(颜色、数字格式、表格样式) + +**示例用法:** +```bash +# 安装技能 +claude plugin install excel-automation@daymade-skills + +# 然后请求 Claude 自动化 Excel 工作流 +"创建一个格式化的估值模板工作簿" +"解析这个 .xlsm 并提取 DCF 工作表" +"生成 Excel 缩放和滚动后截图的 AppleScript 流程" +``` + +**🎬 实时演示** + +*即将推出* + +📚 **文档**:参见 [excel-automation/SKILL.md](./excel-automation/SKILL.md) 和 [formatting-reference.md](./excel-automation/references/formatting-reference.md)。 + +**要求**:Python 3.8+、`uv`、`openpyxl`(通过 `uv run --with openpyxl` 自动安装);AppleScript 窗口控制需要 macOS。 + +--- + +### 41. **capture-screen** - macOS 程序化截图 + +通过三步法实现稳定的窗口截图自动化:Swift 获取 CGWindowID、AppleScript 控制应用状态、`screencapture` 输出截图文件。 + +**使用场景:** +- 为文档或审计流程自动化生成可重复截图 +- 只捕获目标应用窗口而非整屏 +- 在脚本化滚动/缩放后分段截图 +- 构建 macOS 视觉证据采集流水线 + +**主要功能:** +- 内置 Swift 脚本获取准确窗口 ID(`CGWindowListCopyWindowInfo`) +- 提供已验证的 AppleScript 激活与预处理命令模式 +- 支持窗口级静默截图、延时截图与格式控制 +- 提供分段多图采集工作流模板 +- 明确列出在 macOS 上不可用的错误方案,避免踩坑 + +**示例用法:** +```bash +# 安装技能 +claude plugin install capture-screen@daymade-skills + +# 然后请求 Claude 执行程序化截图 +"找到 Excel 窗口 ID 并静默截图" +"为这个工作簿生成分段截图工作流" +"通过脚本滚动后抓取 Chrome 窗口多个区域" +``` + +**🎬 实时演示** + +*即将推出* + +📚 **文档**:参见 [capture-screen/SKILL.md](./capture-screen/SKILL.md)。 + +**要求**:macOS(Swift + AppleScript + `screencapture`)。 + +--- + ## 🎬 交互式演示画廊 想要在一个地方查看所有演示并具有点击放大功能?访问我们的[交互式演示画廊](./demos/index.html)或浏览[演示目录](./demos/)。 @@ -1754,6 +1836,12 @@ claude plugin install financial-data-collector@daymade-skills ### 金融数据与投研 使用 **financial-data-collector** 采集任意美股上市公司的结构化金融数据,将 JSON 输出接入 DCF 建模、可比公司分析或财报复盘工作流。 +### Excel 与财务模型自动化 +使用 **excel-automation** 创建格式化工作簿、解析复杂 `.xlsm` 模型,并自动化 Excel 窗口操作以提升分析效率。 + +### macOS 视觉采集自动化 +使用 **capture-screen** 脚本化执行可重复窗口截图。可与 **excel-automation** 结合生成可直接用于汇报的表格可视化截图。 + ### 会议文档 使用 **meeting-minutes-taker** 将原始会议转写稿转换为结构化、基于证据的会议纪要。与 **transcript-fixer** 结合使用可在生成纪要前清理 ASR 错误。特点是多轮生成配合 UNION 合并以避免内容丢失。 @@ -1851,6 +1939,8 @@ claude plugin install financial-data-collector@daymade-skills - **competitors-analysis**:参见 `competitors-analysis/SKILL.md` 了解证据驱动的分析工作流程和 `competitors-analysis/references/profile_template.md` 了解竞品档案模板 - **windows-remote-desktop-connection-doctor**:参见 `windows-remote-desktop-connection-doctor/references/windows_app_log_analysis.md` 了解日志解析模式和 `windows-remote-desktop-connection-doctor/references/avd_transport_protocols.md` 了解传输协议详情 - **product-analysis**:参见 `product-analysis/SKILL.md` 了解工作流,参见 `product-analysis/references/synthesis_methodology.md` 了解跨代理加权与推荐逻辑 +- **excel-automation**:参见 `excel-automation/SKILL.md` 了解创建/解析/控制工作流,参见 `excel-automation/references/formatting-reference.md` 了解格式规范 +- **capture-screen**:参见 `capture-screen/SKILL.md` 了解基于 CGWindowID 的 macOS 截图流程 ## 🛠️ 系统要求 @@ -1871,6 +1961,8 @@ claude plugin install financial-data-collector@daymade-skills - **Jina.ai API 密钥**(用于 twitter-reader):https://jina.ai/ 提供免费套餐 - **Codex CLI**(可选,用于 product-analysis 多模型并行模式) - **Mole**(可选,用于 macos-cleaner 可视化清理):从 https://github.com/tw93/Mole 下载 +- **uv + openpyxl**(用于 excel-automation):`uv run --with openpyxl ...` +- **macOS**(用于 capture-screen 与 excel-automation 的 AppleScript 控制流程) ## ❓ 常见问题 diff --git a/capture-screen/.security-scan-passed b/capture-screen/.security-scan-passed new file mode 100644 index 0000000..735a6a2 --- /dev/null +++ b/capture-screen/.security-scan-passed @@ -0,0 +1,4 @@ +Security scan passed +Scanned at: 2026-03-02T19:48:47.107251 +Tool: gitleaks + pattern-based validation +Content hash: 7582d78a2119851ab2abb443cf0b0ea3a262a722c28e2522f8a197a064f98bf8 diff --git a/capture-screen/SKILL.md b/capture-screen/SKILL.md new file mode 100644 index 0000000..ab88d26 --- /dev/null +++ b/capture-screen/SKILL.md @@ -0,0 +1,238 @@ +--- +name: capture-screen +description: Programmatic screenshot capture on macOS. Find window IDs with Swift CGWindowListCopyWindowInfo, control application windows via AppleScript (zoom, scroll, select), and capture with screencapture. Use when automating screenshots, capturing application windows for documentation, or building multi-shot visual workflows. +--- + +# Capture Screen + +Programmatic screenshot capture on macOS: find windows, control views, capture images. + +## Quick Start + +```bash +# Find Excel window ID +swift scripts/get_window_id.swift Excel + +# Capture that window (replace 12345 with actual WID) +screencapture -x -l 12345 output.png +``` + +## Overview + +Three-step workflow: + +``` +1. Find Window → Swift CGWindowListCopyWindowInfo → get numeric Window ID +2. Control View → AppleScript (osascript) → zoom, scroll, select +3. Capture → screencapture -l → PNG/JPEG output +``` + +## Step 1: Get Window ID (Swift) + +Use Swift with CoreGraphics to enumerate windows. This is the **only reliable method** on macOS. + +### Quick inline execution + +```bash +swift -e ' +import CoreGraphics +let keyword = "Excel" +let list = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? [] +for w in list { + let owner = w[kCGWindowOwnerName as String] as? String ?? "" + let name = w[kCGWindowName as String] as? String ?? "" + let wid = w[kCGWindowNumber as String] as? Int ?? 0 + if owner.localizedCaseInsensitiveContains(keyword) || name.localizedCaseInsensitiveContains(keyword) { + print("WID=\(wid) | App=\(owner) | Title=\(name)") + } +} +' +``` + +### Using the bundled script + +```bash +swift scripts/get_window_id.swift Excel +swift scripts/get_window_id.swift Chrome +swift scripts/get_window_id.swift # List all windows +``` + +Output format: `WID=12345 | App=Microsoft Excel | Title=workbook.xlsx` + +Parse the WID number for use with `screencapture -l`. + +## Step 2: Control Window (AppleScript) + +Verified commands for controlling application windows before capture. + +### Microsoft Excel (full AppleScript support) + +```bash +# Activate (bring to front) +osascript -e 'tell application "Microsoft Excel" to activate' + +# Set zoom level (percentage) +osascript -e 'tell application "Microsoft Excel" + set zoom of active window to 120 +end tell' + +# Scroll to specific row +osascript -e 'tell application "Microsoft Excel" + set scroll row of active window to 45 +end tell' + +# Scroll to specific column +osascript -e 'tell application "Microsoft Excel" + set scroll column of active window to 3 +end tell' + +# Select a cell range +osascript -e 'tell application "Microsoft Excel" + select range "A1" of active sheet +end tell' + +# Select a specific sheet +osascript -e 'tell application "Microsoft Excel" + activate object sheet "DCF" of active workbook +end tell' + +# Open a file +osascript -e 'tell application "Microsoft Excel" + open POSIX file "/path/to/file.xlsx" +end tell' +``` + +### Any application (basic control) + +```bash +# Activate any app +osascript -e 'tell application "Google Chrome" to activate' + +# Bring specific window to front (by index) +osascript -e 'tell application "System Events" + tell process "Google Chrome" + perform action "AXRaise" of window 1 + end tell +end tell' +``` + +### Timing and Timeout + +Always add `sleep 1` after AppleScript commands before capturing, to allow UI rendering to complete. + +**IMPORTANT**: `osascript` hangs indefinitely if the target application is not running or not responding. Always wrap with `timeout`: + +```bash +timeout 5 osascript -e 'tell application "Microsoft Excel" to activate' +``` + +## Step 3: Capture (screencapture) + +```bash +# Capture specific window by ID +screencapture -l output.png + +# Silent capture (no camera shutter sound) +screencapture -x -l output.png + +# Capture as JPEG +screencapture -l -t jpg output.jpg + +# Capture with delay (seconds) +screencapture -l -T 2 output.png + +# Capture a screen region (interactive) +screencapture -R x,y,width,height output.png +``` + +### Retina displays + +On Retina Macs, `screencapture` outputs 2x resolution by default (e.g., a 2032x1238 window produces a 4064x2476 PNG). This is normal. To get 1x resolution, resize after capture: + +```bash +sips --resampleWidth 2032 output.png --out output_1x.png +``` + +### Verify capture + +```bash +# Check file was created and has content +ls -la output.png +file output.png # Should show "PNG image data, ..." +``` + +## Multi-Shot Workflow + +Complete example: capture multiple sections of an Excel workbook. + +```bash +# 1. Open file and activate Excel +osascript -e 'tell application "Microsoft Excel" + open POSIX file "/path/to/model.xlsx" + activate +end tell' +sleep 2 + +# 2. Set up view +osascript -e 'tell application "Microsoft Excel" + set zoom of active window to 130 + activate object sheet "Summary" of active workbook +end tell' +sleep 1 + +# 3. Get window ID +# IMPORTANT: Always re-fetch before capturing. CGWindowID is invalidated +# when an app restarts or a window is closed and reopened. +WID=$(swift -e ' +import CoreGraphics +let list = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? [] +for w in list { + let owner = w[kCGWindowOwnerName as String] as? String ?? "" + let wid = w[kCGWindowNumber as String] as? Int ?? 0 + if owner == "Microsoft Excel" { print(wid); break } +} +') +echo "Window ID: $WID" + +# 4. Capture Section A (top of sheet) +osascript -e 'tell application "Microsoft Excel" + set scroll row of active window to 1 +end tell' +sleep 1 +screencapture -x -l $WID section_a.png + +# 5. Capture Section B (further down) +osascript -e 'tell application "Microsoft Excel" + set scroll row of active window to 45 +end tell' +sleep 1 +screencapture -x -l $WID section_b.png + +# 6. Switch sheet and capture +osascript -e 'tell application "Microsoft Excel" + activate object sheet "DCF" of active workbook + set scroll row of active window to 1 +end tell' +sleep 1 +screencapture -x -l $WID dcf_overview.png +``` + +## Failed Approaches (DO NOT USE) + +These methods were tested and confirmed to fail on macOS: + +| Method | Error | Why It Fails | +|--------|-------|-------------| +| `System Events` → `id of window` | Error -1728 | System Events cannot access window IDs in the format screencapture needs | +| Python `import Quartz` (PyObjC) | `ModuleNotFoundError` | PyObjC not installed in system Python; don't attempt to install it — use Swift instead | +| `osascript` window id | Wrong format | Returns AppleScript window index, not CGWindowID needed by `screencapture -l` | + +## Supported Applications + +| Application | Window ID | AppleScript Control | Notes | +|------------|-----------|-------------------|-------| +| Microsoft Excel | Swift | Full (zoom, scroll, select, activate sheet) | Best supported | +| Google Chrome | Swift | Basic (activate, window management) | No scroll/zoom via AppleScript | +| Any macOS app | Swift | Basic (activate via `tell application`) | screencapture works universally | + +AppleScript control depth varies by application. Excel has the richest AppleScript dictionary. For apps with limited AppleScript, use keyboard simulation via `System Events` as a fallback. diff --git a/capture-screen/scripts/get_window_id.swift b/capture-screen/scripts/get_window_id.swift new file mode 100755 index 0000000..c19ef7f --- /dev/null +++ b/capture-screen/scripts/get_window_id.swift @@ -0,0 +1,57 @@ +#!/usr/bin/env swift +// +// get_window_id.swift +// Enumerate on-screen windows and print their Window IDs. +// +// Usage: +// swift get_window_id.swift # List all windows +// swift get_window_id.swift Excel # Filter by keyword +// swift get_window_id.swift "Chrome" # Filter by app name +// +// Output format: +// WID=12345 | App=Microsoft Excel | Title=workbook.xlsx +// +// The WID value is compatible with: screencapture -l output.png +// + +import CoreGraphics + +let keyword = CommandLine.arguments.count > 1 + ? CommandLine.arguments[1] + : "" + +guard let windowList = CGWindowListCopyWindowInfo( + .optionOnScreenOnly, kCGNullWindowID +) as? [[String: Any]] else { + fputs("ERROR: Failed to enumerate windows.\n", stderr) + fputs("Possible causes:\n", stderr) + fputs(" - No applications with visible windows are running\n", stderr) + fputs(" - Screen Recording permission not granted (System Settings → Privacy & Security → Screen Recording)\n", stderr) + exit(1) +} + +var found = false +for w in windowList { + let owner = w[kCGWindowOwnerName as String] as? String ?? "" + let name = w[kCGWindowName as String] as? String ?? "" + let wid = w[kCGWindowNumber as String] as? Int ?? 0 + + // Skip windows without a title (menu bar items, system UI, etc.) + if name.isEmpty && !keyword.isEmpty { continue } + + if keyword.isEmpty + || owner.localizedCaseInsensitiveContains(keyword) + || name.localizedCaseInsensitiveContains(keyword) { + print("WID=\(wid) | App=\(owner) | Title=\(name)") + found = true + } +} + +if !found && !keyword.isEmpty { + fputs("No windows found matching '\(keyword)'\n", stderr) + fputs("Troubleshooting:\n", stderr) + fputs(" - Is the application running? (check: pgrep -i '\(keyword)')\n", stderr) + fputs(" - Is the window visible (not minimized to Dock)?\n", stderr) + fputs(" - Try without keyword to see all windows: swift get_window_id.swift\n", stderr) + exit(1) +} diff --git a/excel-automation/.security-scan-passed b/excel-automation/.security-scan-passed new file mode 100644 index 0000000..c7a354e --- /dev/null +++ b/excel-automation/.security-scan-passed @@ -0,0 +1,4 @@ +Security scan passed +Scanned at: 2026-03-02T19:48:47.112631 +Tool: gitleaks + pattern-based validation +Content hash: e2af503b1847702eb50d3883c1a6e7bfb3f870edd786c740dee34a49611f4f6f diff --git a/excel-automation/SKILL.md b/excel-automation/SKILL.md new file mode 100644 index 0000000..94b55f6 --- /dev/null +++ b/excel-automation/SKILL.md @@ -0,0 +1,313 @@ +--- +name: excel-automation +description: Create, parse, and control Excel files on macOS. Professional formatting with openpyxl, complex xlsm parsing with stdlib zipfile+xml for investment bank financial models, and Excel window control via AppleScript. Use when creating formatted Excel reports, parsing financial models that openpyxl cannot handle, or automating Excel on macOS. +--- + +# Excel Automation + +Create professional Excel files, parse complex financial models, and control Excel on macOS. + +## Quick Start + +```bash +# Create a formatted Excel report +uv run --with openpyxl scripts/create_formatted_excel.py output.xlsx + +# Parse a complex xlsm that openpyxl can't handle +uv run scripts/parse_complex_excel.py model.xlsm # List sheets +uv run scripts/parse_complex_excel.py model.xlsm "DCF" # Extract a sheet +uv run scripts/parse_complex_excel.py model.xlsm --fix # Fix corrupted names + +# Control Excel via AppleScript (with timeout to prevent hangs) +timeout 5 osascript -e 'tell application "Microsoft Excel" to activate' +``` + +## Overview + +Three capabilities: + +| Capability | Tool | When to Use | +|-----------|------|-------------| +| **Create** formatted Excel | `openpyxl` | Reports, mockups, dashboards | +| **Parse** complex xlsm/xlsx | `zipfile` + `xml.etree` | Financial models, VBA workbooks, >1MB files | +| **Control** Excel window | AppleScript (`osascript`) | Zoom, scroll, select cells programmatically | + +## Tool Selection Decision Tree + +``` +Is the file simple (data export, no VBA, <1MB)? +├─ YES → openpyxl or pandas +└─ NO + ├─ Is it .xlsm or from investment bank / >1MB? + │ └─ YES → zipfile + xml.etree.ElementTree (stdlib) + └─ Is it truly .xls (BIFF format)? + └─ YES → xlrd +``` + +**Signals of "complex" Excel**: file >1MB, `.xlsm` extension, from investment bank/broker, contains VBA macros. + +**IMPORTANT**: Always run `file ` first — extensions lie. A `.xls` file may actually be a ZIP-based xlsx. + +## Creating Excel Files (openpyxl) + +### Professional Color Convention (Investment Banking Standard) + +| Color | RGB Code | Meaning | +|-------|----------|---------| +| Blue | `0000FF` | User input / assumption | +| Black | `000000` | Calculated value | +| Green | `008000` | Cross-sheet reference | +| White on dark blue | `FFFFFF` on `4472C4` | Section headers | +| Dark blue text | `1F4E79` | Title | + +### Core Formatting Patterns + +```python +from openpyxl.styles import Font, PatternFill, Border, Side, Alignment + +# Fonts +BLUE_FONT = Font(color="0000FF", size=10, name="Calibri") +BLACK_FONT_BOLD = Font(color="000000", size=10, name="Calibri", bold=True) +GREEN_FONT = Font(color="008000", size=10, name="Calibri") +HEADER_FONT = Font(color="FFFFFF", size=12, name="Calibri", bold=True) + +# Fills +DARK_BLUE_FILL = PatternFill("solid", fgColor="4472C4") +LIGHT_BLUE_FILL = PatternFill("solid", fgColor="D9E1F2") +INPUT_GREEN_FILL = PatternFill("solid", fgColor="E2EFDA") +LIGHT_GRAY_FILL = PatternFill("solid", fgColor="F2F2F2") + +# Borders +THIN_BORDER = Border(bottom=Side(style="thin", color="B2B2B2")) +BOTTOM_DOUBLE = Border(bottom=Side(style="double", color="000000")) +``` + +### Number Format Codes + +| Format | Code | Example | +|--------|------|---------| +| Currency | `'$#,##0'` | $1,234 | +| Currency with decimals | `'$#,##0.00'` | $1,234.56 | +| Percentage | `'0.0%'` | 12.3% | +| Percentage (2 decimal) | `'0.00%'` | 12.34% | +| Number with commas | `'#,##0'` | 1,234 | +| Multiplier | `'0.0x'` | 1.5x | + +### Conditional Formatting (Sensitivity Tables) + +Red-to-green gradient for sensitivity analysis: + +```python +from openpyxl.formatting.rule import ColorScaleRule + +rule = ColorScaleRule( + start_type="min", start_color="F8696B", # Red (low) + mid_type="percentile", mid_value=50, mid_color="FFEB84", # Yellow (mid) + end_type="max", end_color="63BE7B" # Green (high) +) +ws.conditional_formatting.add(f"B2:F6", rule) +``` + +### Execution + +```bash +uv run --with openpyxl scripts/create_formatted_excel.py +``` + +Full template script: See `scripts/create_formatted_excel.py` + +## Parsing Complex Excel (zipfile + xml) + +When openpyxl fails on complex xlsm files (corrupted DefinedNames, complex VBA), use stdlib directly. + +### XLSX Internal ZIP Structure + +``` +file.xlsx (ZIP archive) +├── [Content_Types].xml +├── xl/ +│ ├── workbook.xml ← Sheet names + order +│ ├── sharedStrings.xml ← All text values (lookup table) +│ ├── worksheets/ +│ │ ├── sheet1.xml ← Cell data for sheet 1 +│ │ ├── sheet2.xml ← Cell data for sheet 2 +│ │ └── ... +│ └── _rels/ +│ └── workbook.xml.rels ← Maps rId → sheetN.xml +└── _rels/.rels +``` + +### Sheet Name Resolution (Two-Step) + +Sheet names in `workbook.xml` link to physical files via `_rels/workbook.xml.rels`: + +```python +import zipfile +import xml.etree.ElementTree as ET + +MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' +REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' +RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships' + +def get_sheet_path(zf, sheet_name): + """Resolve sheet name to physical XML file path inside ZIP.""" + # Step 1: workbook.xml → find rId for the sheet name + wb_xml = ET.fromstring(zf.read('xl/workbook.xml')) + sheets = wb_xml.findall(f'.//{{{MAIN_NS}}}sheet') + rid = None + for s in sheets: + if s.get('name') == sheet_name: + rid = s.get(f'{{{REL_NS}}}id') + break + if not rid: + raise ValueError(f"Sheet '{sheet_name}' not found") + + # Step 2: workbook.xml.rels → map rId to file path + rels_xml = ET.fromstring(zf.read('xl/_rels/workbook.xml.rels')) + for rel in rels_xml.findall(f'{{{RELS_NS}}}Relationship'): + if rel.get('Id') == rid: + return 'xl/' + rel.get('Target') + + raise ValueError(f"No file mapping for {rid}") +``` + +### Cell Data Extraction + +```python +def extract_cells(zf, sheet_path): + """Extract all cell values from a sheet XML.""" + # Build shared strings lookup + shared = [] + try: + ss_xml = ET.fromstring(zf.read('xl/sharedStrings.xml')) + for si in ss_xml.findall(f'{{{MAIN_NS}}}si'): + texts = si.itertext() + shared.append(''.join(texts)) + except KeyError: + pass # No shared strings + + # Parse sheet cells + sheet_xml = ET.fromstring(zf.read(sheet_path)) + rows = sheet_xml.findall(f'.//{{{MAIN_NS}}}row') + + data = {} + for row in rows: + for cell in row.findall(f'{{{MAIN_NS}}}c'): + ref = cell.get('r') # e.g., "A1" + cell_type = cell.get('t') # "s" = shared string, None = number + val_el = cell.find(f'{{{MAIN_NS}}}v') + + if val_el is not None and val_el.text: + if cell_type == 's': + data[ref] = shared[int(val_el.text)] + else: + try: + data[ref] = float(val_el.text) + except ValueError: + data[ref] = val_el.text + return data +``` + +### Fixing Corrupted DefinedNames + +Investment bank xlsm files often have corrupted `` entries containing "Formula removed": + +```python +def fix_defined_names(zf_in_path, zf_out_path): + """Remove corrupted DefinedNames and repackage.""" + import shutil, tempfile + with tempfile.TemporaryDirectory() as tmp: + tmp = Path(tmp) + with zipfile.ZipFile(zf_in_path, 'r') as zf: + zf.extractall(tmp) + + wb_xml_path = tmp / 'xl' / 'workbook.xml' + tree = ET.parse(wb_xml_path) + root = tree.getroot() + + ns = {'main': MAIN_NS} + defined_names = root.find('.//main:definedNames', ns) + if defined_names is not None: + for name in list(defined_names): + if name.text and "Formula removed" in name.text: + defined_names.remove(name) + + tree.write(wb_xml_path, encoding='utf-8', xml_declaration=True) + + with zipfile.ZipFile(zf_out_path, 'w', zipfile.ZIP_DEFLATED) as zf: + for fp in tmp.rglob('*'): + if fp.is_file(): + zf.write(fp, fp.relative_to(tmp)) +``` + +Full template script: See `scripts/parse_complex_excel.py` + +## Controlling Excel on macOS (AppleScript) + +All commands verified on macOS with Microsoft Excel. + +### Verified Commands + +```bash +# Activate Excel (bring to front) +osascript -e 'tell application "Microsoft Excel" to activate' + +# Open a file +osascript -e 'tell application "Microsoft Excel" to open POSIX file "/path/to/file.xlsx"' + +# Set zoom level (percentage) +osascript -e 'tell application "Microsoft Excel" + set zoom of active window to 120 +end tell' + +# Scroll to specific row +osascript -e 'tell application "Microsoft Excel" + set scroll row of active window to 45 +end tell' + +# Scroll to specific column +osascript -e 'tell application "Microsoft Excel" + set scroll column of active window to 3 +end tell' + +# Select a cell range +osascript -e 'tell application "Microsoft Excel" + select range "A1" of active sheet +end tell' + +# Select a specific sheet by name +osascript -e 'tell application "Microsoft Excel" + activate object sheet "DCF" of active workbook +end tell' +``` + +### Timing and Timeout + +Always add `sleep 1` between AppleScript commands and subsequent operations (e.g., screenshot) to allow UI rendering. + +**IMPORTANT**: `osascript` will hang indefinitely if Excel is not running or not responding. Always wrap with `timeout`: + +```bash +# Safe pattern: 5-second timeout +timeout 5 osascript -e 'tell application "Microsoft Excel" to activate' + +# Check exit code: 124 = timed out +if [ $? -eq 124 ]; then + echo "Excel not responding — is it running?" +fi +``` + +## Common Mistakes + +| Mistake | Correction | +|---------|-----------| +| openpyxl fails on complex xlsm → try monkey-patching | Switch to `zipfile` + `xml.etree` immediately | +| Count Chinese characters with `wc -c` | Use `wc -m` (chars, not bytes; Chinese = 3 bytes/char) | +| Trust file extension | Run `file ` first to confirm actual format | +| openpyxl `load_workbook` hangs on large xlsm | Use `zipfile` for targeted extraction instead of loading entire workbook | + +## Important Notes + +- Execute Python scripts with `uv run --with openpyxl` (never use system Python) +- LibreOffice (`soffice --headless`) can convert formats and recalculate formulas +- Detailed formatting reference: See `references/formatting-reference.md` diff --git a/excel-automation/references/formatting-reference.md b/excel-automation/references/formatting-reference.md new file mode 100644 index 0000000..663ec43 --- /dev/null +++ b/excel-automation/references/formatting-reference.md @@ -0,0 +1,122 @@ +# Excel Formatting Quick Reference + +## Font Colors (RGB) + +| Use Case | Color Name | RGB Code | openpyxl | +|----------|-----------|----------|----------| +| User input / assumption | Blue | `0000FF` | `Font(color="0000FF")` | +| Calculated value | Black | `000000` | `Font(color="000000")` | +| Cross-sheet reference | Green | `008000` | `Font(color="008000")` | +| Section header text | White | `FFFFFF` | `Font(color="FFFFFF")` | +| Title text | Dark Blue | `1F4E79` | `Font(color="1F4E79")` | +| Subtitle text | Dark Gray | `404040` | `Font(color="404040")` | +| Negative values | Red | `FF0000` | `Font(color="FF0000")` | + +## Fill Colors (RGB) + +| Use Case | Color Name | RGB Code | openpyxl | +|----------|-----------|----------|----------| +| Section header background | Dark Blue | `4472C4` | `PatternFill("solid", fgColor="4472C4")` | +| Sub-header / alternating row | Light Blue | `D9E1F2` | `PatternFill("solid", fgColor="D9E1F2")` | +| Input cell highlight | Light Green | `E2EFDA` | `PatternFill("solid", fgColor="E2EFDA")` | +| Clean background | White | `FFFFFF` | `PatternFill("solid", fgColor="FFFFFF")` | +| Alternating row | Light Gray | `F2F2F2` | `PatternFill("solid", fgColor="F2F2F2")` | + +## Sensitivity Table Gradient Fills + +| Level | Color Name | RGB Code | Meaning | +|-------|-----------|----------|---------| +| Worst | Deep Red | `F4CCCC` | Far below target | +| Below | Light Red | `FCE4D6` | Below target | +| Neutral | Light Yellow | `FFF2CC` | Near target | +| Above | Light Green | `D9EAD3` | Above target | +| Best | Deep Green | `B6D7A8` | Far above target | + +## Conditional Formatting (ColorScaleRule) + +```python +from openpyxl.formatting.rule import ColorScaleRule + +# Red → Yellow → Green (3-color scale) +rule = ColorScaleRule( + start_type="min", start_color="F8696B", + mid_type="percentile", mid_value=50, mid_color="FFEB84", + end_type="max", end_color="63BE7B", +) +ws.conditional_formatting.add("B2:F6", rule) + +# Red → Green (2-color scale) +rule = ColorScaleRule( + start_type="min", start_color="F8696B", + end_type="max", end_color="63BE7B", +) +``` + +## Number Format Strings + +| Format | Code | Example Output | +|--------|------|---------------| +| Currency (no decimals) | `'$#,##0'` | $1,234 | +| Currency (2 decimals) | `'$#,##0.00'` | $1,234.56 | +| Thousands | `'$#,##0,"K"'` | $1,234K | +| Millions | `'$#,##0.0,,"M"'` | $1.2M | +| Percentage (1 decimal) | `'0.0%'` | 12.3% | +| Percentage (2 decimals) | `'0.00%'` | 12.34% | +| Number with commas | `'#,##0'` | 1,234 | +| Multiplier | `'0.0x'` | 1.5x | +| Accounting | `'_($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)'` | $ 1,234 | +| Date | `'YYYY-MM-DD'` | 2026-03-01 | +| Negative in red | `'#,##0;[Red]-#,##0'` | -1,234 (red) | + +## Border Styles + +| Style | Side Code | Use Case | +|-------|----------|----------| +| Thin gray | `Side(style="thin", color="B2B2B2")` | Row separators | +| Medium black | `Side(style="medium", color="000000")` | Section separators | +| Double black | `Side(style="double", color="000000")` | Final totals | +| Thin black | `Side(style="thin", color="000000")` | Grid lines | + +Available `style` values: `'thin'`, `'medium'`, `'thick'`, `'double'`, `'dotted'`, `'dashed'`, `'hair'` + +## Column Width Guidelines + +| Content Type | Recommended Width | +|-------------|------------------| +| Short label | 12-15 | +| Currency value | 14-16 | +| Percentage | 10-12 | +| Date | 12 | +| Long description | 25-35 | +| Auto-fit formula | `max(10, min(len(str(max_value)) + 2, 20))` | + +## Alignment Presets + +```python +from openpyxl.styles import Alignment + +CENTER = Alignment(horizontal="center", vertical="center") +RIGHT = Alignment(horizontal="right", vertical="center") +LEFT = Alignment(horizontal="left", vertical="center") +WRAP = Alignment(horizontal="left", vertical="top", wrap_text=True) +``` + +## Font Presets (Calibri 10pt Base) + +```python +from openpyxl.styles import Font + +# Standard +REGULAR = Font(size=10, name="Calibri") +BOLD = Font(size=10, name="Calibri", bold=True) +ITALIC = Font(size=10, name="Calibri", italic=True) + +# Headers +SECTION_HEADER = Font(size=12, name="Calibri", bold=True, color="FFFFFF") +TITLE = Font(size=14, name="Calibri", bold=True, color="1F4E79") + +# Semantic (investment banking convention) +INPUT = Font(size=10, name="Calibri", color="0000FF") # User inputs +CALC = Font(size=10, name="Calibri", color="000000") # Formulas +XREF = Font(size=10, name="Calibri", color="008000") # Cross-references +``` diff --git a/excel-automation/scripts/create_formatted_excel.py b/excel-automation/scripts/create_formatted_excel.py new file mode 100755 index 0000000..5b1d5b7 --- /dev/null +++ b/excel-automation/scripts/create_formatted_excel.py @@ -0,0 +1,259 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["openpyxl"] +# /// +""" +Create a professionally formatted Excel workbook with investment banking +standard styling. + +Usage: + uv run scripts/create_formatted_excel.py [output_path] + +This is a reusable template. Adapt the data section for your use case. +""" + +import sys +from pathlib import Path +from openpyxl import Workbook +from openpyxl.styles import ( + Alignment, + Border, + Font, + PatternFill, + Side, +) +from openpyxl.formatting.rule import ColorScaleRule +from openpyxl.utils import get_column_letter + + +# ── Color Palette (Investment Banking Standard) ────────────────────── + +# Fonts +BLUE_FONT = Font(color="0000FF", size=10, name="Calibri") +BLUE_FONT_BOLD = Font(color="0000FF", size=10, name="Calibri", bold=True) +BLACK_FONT = Font(color="000000", size=10, name="Calibri") +BLACK_FONT_BOLD = Font(color="000000", size=10, name="Calibri", bold=True) +GREEN_FONT = Font(color="008000", size=10, name="Calibri") +GREEN_FONT_BOLD = Font(color="008000", size=10, name="Calibri", bold=True) +WHITE_FONT_BOLD = Font(color="FFFFFF", size=10, name="Calibri", bold=True) +HEADER_FONT = Font(color="FFFFFF", size=12, name="Calibri", bold=True) +TITLE_FONT = Font(color="1F4E79", size=14, name="Calibri", bold=True) +SUBTITLE_FONT = Font(color="404040", size=10, name="Calibri", italic=True) + +# Fills +DARK_BLUE_FILL = PatternFill("solid", fgColor="4472C4") +LIGHT_BLUE_FILL = PatternFill("solid", fgColor="D9E1F2") +INPUT_GREEN_FILL = PatternFill("solid", fgColor="E2EFDA") +WHITE_FILL = PatternFill("solid", fgColor="FFFFFF") +LIGHT_GRAY_FILL = PatternFill("solid", fgColor="F2F2F2") + +# Sensitivity gradient fills (manual, for when conditional formatting isn't suitable) +SENS_DEEP_RED = PatternFill("solid", fgColor="F4CCCC") +SENS_LIGHT_RED = PatternFill("solid", fgColor="FCE4D6") +SENS_NEUTRAL = PatternFill("solid", fgColor="FFF2CC") +SENS_LIGHT_GREEN = PatternFill("solid", fgColor="D9EAD3") +SENS_DEEP_GREEN = PatternFill("solid", fgColor="B6D7A8") + +# Borders +THIN_BORDER = Border(bottom=Side(style="thin", color="B2B2B2")) +BOTTOM_MEDIUM = Border(bottom=Side(style="medium", color="000000")) +BOTTOM_DOUBLE = Border(bottom=Side(style="double", color="000000")) +ALL_THIN = Border( + left=Side(style="thin", color="B2B2B2"), + right=Side(style="thin", color="B2B2B2"), + top=Side(style="thin", color="B2B2B2"), + bottom=Side(style="thin", color="B2B2B2"), +) + +# Alignment +CENTER = Alignment(horizontal="center", vertical="center") +RIGHT = Alignment(horizontal="right", vertical="center") +LEFT = Alignment(horizontal="left", vertical="center") + + +# ── Helper Functions ───────────────────────────────────────────────── + +def apply_header_row(ws, row, labels, start_col=1): + """Apply dark blue header styling to a row of labels.""" + for i, label in enumerate(labels): + cell = ws.cell(row=row, column=start_col + i, value=label) + cell.font = WHITE_FONT_BOLD + cell.fill = DARK_BLUE_FILL + cell.alignment = CENTER + + +def apply_data_row(ws, row, values, start_col=1, font=None, number_format=None, + fill=None, border=None): + """Write a row of values with consistent formatting.""" + font = font or BLACK_FONT + for i, val in enumerate(values): + cell = ws.cell(row=row, column=start_col + i, value=val) + cell.font = font + if number_format: + cell.number_format = number_format + if fill: + cell.fill = fill + if border: + cell.border = border + cell.alignment = RIGHT if isinstance(val, (int, float)) else LEFT + + +def apply_input_cell(ws, row, col, value, number_format=None): + """Style a cell as user input (blue font, green fill).""" + cell = ws.cell(row=row, column=col, value=value) + cell.font = BLUE_FONT + cell.fill = INPUT_GREEN_FILL + if number_format: + cell.number_format = number_format + return cell + + +def add_sensitivity_table(ws, start_row, start_col, row_header, col_header, + row_values, col_values, data_matrix): + """ + Create a sensitivity table with conditional formatting. + + Args: + ws: Worksheet + start_row/start_col: Top-left corner of the table + row_header/col_header: Labels for the axes + row_values: List of values for rows (e.g., WACC rates) + col_values: List of values for columns (e.g., growth rates) + data_matrix: 2D list of result values + """ + # Column header label + ws.cell(row=start_row, column=start_col + 1, value=col_header).font = BLACK_FONT_BOLD + + # Column values + for j, cv in enumerate(col_values): + cell = ws.cell(row=start_row, column=start_col + 1 + j, value=cv) + cell.font = BLUE_FONT_BOLD + cell.alignment = CENTER + + # Row header label + ws.cell(row=start_row + 1, column=start_col, value=row_header).font = BLACK_FONT_BOLD + + # Data cells + for i, rv in enumerate(row_values): + # Row label + cell = ws.cell(row=start_row + 1 + i, column=start_col, value=rv) + cell.font = BLUE_FONT_BOLD + cell.alignment = CENTER + + for j, dv in enumerate(data_matrix[i]): + cell = ws.cell(row=start_row + 1 + i, column=start_col + 1 + j, value=dv) + cell.font = BLACK_FONT + cell.number_format = '$#,##0' + cell.alignment = CENTER + cell.border = ALL_THIN + + # Apply conditional formatting (red-yellow-green gradient) + data_range = ( + f"{get_column_letter(start_col + 1)}{start_row + 1}:" + f"{get_column_letter(start_col + len(col_values))}{start_row + len(row_values)}" + ) + rule = ColorScaleRule( + start_type="min", start_color="F8696B", + mid_type="percentile", mid_value=50, mid_color="FFEB84", + end_type="max", end_color="63BE7B", + ) + ws.conditional_formatting.add(data_range, rule) + + +def auto_column_widths(ws, min_width=10, max_width=20): + """Auto-adjust column widths based on content. + + CJK characters occupy ~2 character widths in Excel, so we count them + as 2 instead of 1 to avoid truncated columns. + """ + for col_cells in ws.columns: + max_len = 0 + col_letter = get_column_letter(col_cells[0].column) + for cell in col_cells: + if cell.value: + s = str(cell.value) + # CJK chars (U+4E00–U+9FFF, fullwidth, etc.) occupy ~2 widths + width = sum(2 if '\u4e00' <= c <= '\u9fff' or + '\u3000' <= c <= '\u303f' or + '\uff00' <= c <= '\uffef' else 1 + for c in s) + max_len = max(max_len, width) + ws.column_dimensions[col_letter].width = max(min_width, min(max_len + 2, max_width)) + + +# ── Example: Create a DCF Summary ─────────────────────────────────── + +def create_example_workbook(output_path: str): + """Create an example professionally formatted Excel workbook.""" + wb = Workbook() + ws = wb.active + ws.title = "DCF Summary" + + # Title + ws.cell(row=1, column=1, value="DCF Valuation Summary").font = TITLE_FONT + ws.cell(row=2, column=1, value="Example Company — Base Case").font = SUBTITLE_FONT + + # Key assumptions header + apply_header_row(ws, 4, ["Parameter", "Value", "Source"]) + + # Key assumptions data + assumptions = [ + ("WACC", 0.10, "Calculated"), + ("Terminal Growth Rate", 0.03, "Assumption"), + ("Shares Outstanding (M)", 2580, "10-K Filing"), + ("Net Debt ($M)", 28000, "Balance Sheet"), + ] + for i, (param, value, source) in enumerate(assumptions): + r = 5 + i + ws.cell(row=r, column=1, value=param).font = BLACK_FONT + apply_input_cell(ws, r, 2, value, + number_format='0.0%' if isinstance(value, float) and value < 1 else '#,##0') + ws.cell(row=r, column=3, value=source).font = GREEN_FONT + + # Separator + for col in range(1, 4): + ws.cell(row=9, column=col).border = BOTTOM_MEDIUM + + # Valuation output + ws.cell(row=10, column=1, value="Implied Share Price").font = BLACK_FONT_BOLD + cell = ws.cell(row=10, column=2, value=580) + cell.font = BLACK_FONT_BOLD + cell.number_format = '$#,##0' + cell.border = BOTTOM_DOUBLE + + # Sensitivity table + ws.cell(row=12, column=1, value="Sensitivity Analysis").font = TITLE_FONT + + wacc_values = [0.08, 0.09, 0.10, 0.11, 0.12] + growth_values = [0.01, 0.02, 0.03, 0.04, 0.05] + # Example data matrix (WACC rows x Growth cols) + data_matrix = [ + [720, 780, 850, 940, 1050], + [640, 690, 740, 800, 870], + [570, 610, 650, 700, 750], + [510, 540, 580, 620, 660], + [460, 490, 520, 550, 580], + ] + + add_sensitivity_table( + ws, start_row=14, start_col=1, + row_header="WACC", col_header="Terminal Growth Rate", + row_values=wacc_values, col_values=growth_values, + data_matrix=data_matrix, + ) + + # Format WACC/growth as percentages + for r in range(15, 20): + ws.cell(row=r, column=1).number_format = '0.0%' + for c in range(2, 7): + ws.cell(row=14, column=c).number_format = '0.0%' + + auto_column_widths(ws) + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + wb.save(output_path) + print(f"Created: {output_path}") + + +if __name__ == "__main__": + output = sys.argv[1] if len(sys.argv) > 1 else "example_output.xlsx" + create_example_workbook(output) diff --git a/excel-automation/scripts/parse_complex_excel.py b/excel-automation/scripts/parse_complex_excel.py new file mode 100755 index 0000000..de2334a --- /dev/null +++ b/excel-automation/scripts/parse_complex_excel.py @@ -0,0 +1,278 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// +""" +Parse complex xlsx/xlsm files using stdlib zipfile + xml.etree. + +No external dependencies required — uses only Python standard library. + +Usage: + uv run scripts/parse_complex_excel.py [sheet_name] + +This handles files that openpyxl cannot open (corrupted DefinedNames, +complex VBA macros, investment bank financial models). +""" + +import json +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +# XML namespaces used in Office Open XML +MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' +REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' +RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships' + + +def verify_format(file_path: str) -> str: + """Verify actual file format using the `file` command.""" + result = subprocess.run( + ['file', '--brief', file_path], + capture_output=True, text=True + ) + return result.stdout.strip() + + +def list_sheets(zf: zipfile.ZipFile) -> list[dict]: + """List all sheet names and their physical XML paths.""" + wb_xml = ET.fromstring(zf.read('xl/workbook.xml')) + sheets_el = wb_xml.findall(f'.//{{{MAIN_NS}}}sheet') + + rels_xml = ET.fromstring(zf.read('xl/_rels/workbook.xml.rels')) + rid_to_path = {} + for rel in rels_xml.findall(f'{{{RELS_NS}}}Relationship'): + rid_to_path[rel.get('Id')] = 'xl/' + rel.get('Target') + + sheets = [] + for s in sheets_el: + name = s.get('name') + rid = s.get(f'{{{REL_NS}}}id') + path = rid_to_path.get(rid, '?') + sheets.append({'name': name, 'rId': rid, 'path': path}) + + return sheets + + +def get_sheet_path(zf: zipfile.ZipFile, sheet_name: str) -> str: + """Resolve a sheet name to its physical XML path inside the ZIP.""" + # Step 1: workbook.xml — find rId for the named sheet + wb_xml = ET.fromstring(zf.read('xl/workbook.xml')) + sheets = wb_xml.findall(f'.//{{{MAIN_NS}}}sheet') + rid = None + for s in sheets: + if s.get('name') == sheet_name: + rid = s.get(f'{{{REL_NS}}}id') + break + if not rid: + available = [s.get('name') for s in sheets] + raise ValueError( + f"Sheet '{sheet_name}' not found. Available: {available}" + ) + + # Step 2: workbook.xml.rels — map rId to file path + rels_xml = ET.fromstring(zf.read('xl/_rels/workbook.xml.rels')) + for rel in rels_xml.findall(f'{{{RELS_NS}}}Relationship'): + if rel.get('Id') == rid: + return 'xl/' + rel.get('Target') + + raise ValueError(f"No file mapping for {rid}") + + +def build_shared_strings(zf: zipfile.ZipFile) -> list[str]: + """Build the shared strings lookup table.""" + shared = [] + try: + ss_xml = ET.fromstring(zf.read('xl/sharedStrings.xml')) + for si in ss_xml.findall(f'{{{MAIN_NS}}}si'): + shared.append(''.join(si.itertext())) + except KeyError: + pass # No shared strings in this file + return shared + + +def parse_cell_ref(ref: str) -> tuple[str, int]: + """Parse 'AB123' into ('AB', 123).""" + match = re.match(r'^([A-Z]+)(\d+)$', ref) + if not match: + return ref, 0 + return match.group(1), int(match.group(2)) + + +def extract_cells(zf: zipfile.ZipFile, sheet_path: str, + shared: list[str]) -> dict[str, any]: + """Extract all cell values from a sheet XML.""" + sheet_xml = ET.fromstring(zf.read(sheet_path)) + rows = sheet_xml.findall(f'.//{{{MAIN_NS}}}row') + + data = {} + for row in rows: + for cell in row.findall(f'{{{MAIN_NS}}}c'): + ref = cell.get('r') + cell_type = cell.get('t') # "s" = shared string, None = number + val_el = cell.find(f'{{{MAIN_NS}}}v') + + if val_el is not None and val_el.text: + if cell_type == 's': + idx = int(val_el.text) + data[ref] = shared[idx] if idx < len(shared) else f'[SSI:{idx}]' + elif cell_type == 'b': + data[ref] = bool(int(val_el.text)) + else: + try: + num = float(val_el.text) + data[ref] = int(num) if num == int(num) else num + except ValueError: + data[ref] = val_el.text + + return data + + +def extract_rows(cells: dict, start_row: int = 1, + end_row: int | None = None) -> list[dict]: + """Organize cells into row-based structure for easier consumption.""" + # Determine row range + all_rows = set() + for ref in cells: + _, row_num = parse_cell_ref(ref) + if row_num > 0: + all_rows.add(row_num) + + if not all_rows: + return [] + + start = max(start_row, min(all_rows)) + end = min(end_row, max(all_rows)) if end_row else max(all_rows) + + rows = [] + for r in range(start, end + 1): + row_cells = { + ref: val for ref, val in cells.items() + if parse_cell_ref(ref)[1] == r + } + if row_cells: + rows.append({'row': r, 'cells': row_cells}) + + return rows + + +def fix_defined_names(input_path: str, output_path: str) -> int: + """ + Remove corrupted DefinedNames entries (containing "Formula removed") + and repackage the file. + + Returns the number of removed entries. + """ + import shutil + import tempfile + + with tempfile.TemporaryDirectory() as tmp_str: + tmp = Path(tmp_str) + + # Extract + with zipfile.ZipFile(input_path, 'r') as zf: + zf.extractall(tmp) + + # Fix workbook.xml + wb_path = tmp / 'xl' / 'workbook.xml' + tree = ET.parse(wb_path) + root = tree.getroot() + + ns = {'main': MAIN_NS} + defined_names = root.find('.//main:definedNames', ns) + removed = 0 + if defined_names is not None: + for name in list(defined_names): + if name.text and "Formula removed" in name.text: + defined_names.remove(name) + removed += 1 + + tree.write(wb_path, encoding='utf-8', xml_declaration=True) + + # Repackage + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf: + for fp in tmp.rglob('*'): + if fp.is_file(): + zf.write(fp, fp.relative_to(tmp)) + + return removed + + +# ── CLI Entry Point ────────────────────────────────────────────────── + +def main(): + if len(sys.argv) < 2: + print("Usage: parse_complex_excel.py [sheet_name]") + print("\nExamples:") + print(" parse_complex_excel.py model.xlsm # List all sheets") + print(" parse_complex_excel.py model.xlsm DCF # Extract DCF sheet") + print(" parse_complex_excel.py model.xlsm --fix # Fix corrupted names") + sys.exit(1) + + file_path = sys.argv[1] + path = Path(file_path) + + if not path.exists(): + print(f"File not found: {file_path}") + sys.exit(1) + + # Verify format + fmt = verify_format(file_path) + print(f"File: {path.name}") + print(f"Format: {fmt}") + + # "Microsoft Excel 2007+" = ZIP-based xlsx/xlsm + # "Zip archive" = generic ZIP (also valid) + # "Composite Document File" = old BIFF .xls format + is_zip_based = any(kw in fmt.lower() for kw in ['zip', 'excel 2007', 'ooxml']) + if not is_zip_based: + print("WARNING: File is not ZIP-based xlsx/xlsm.") + if 'composite' in fmt.lower() or 'biff' in fmt.lower(): + print("This appears to be an old .xls (BIFF format). Use xlrd instead.") + else: + print(f"Unexpected format. If it should be xlsx/xlsm, check the file.") + sys.exit(1) + + # Handle --fix flag + if len(sys.argv) > 2 and sys.argv[2] == '--fix': + out_path = str(path.with_stem(path.stem + '_fixed')) + removed = fix_defined_names(file_path, out_path) + print(f"Removed {removed} corrupted DefinedNames entries.") + print(f"Fixed file: {out_path}") + sys.exit(0) + + with zipfile.ZipFile(file_path, 'r') as zf: + # List sheets + sheets = list_sheets(zf) + print(f"\nSheets ({len(sheets)}):") + for i, s in enumerate(sheets, 1): + print(f" {i}. {s['name']} → {s['path']}") + + # If sheet name given, extract it + if len(sys.argv) > 2: + sheet_name = sys.argv[2] + print(f"\nExtracting sheet: {sheet_name}") + + sheet_path = get_sheet_path(zf, sheet_name) + shared = build_shared_strings(zf) + cells = extract_cells(zf, sheet_path, shared) + + print(f"Total cells: {len(cells)}") + + # Show first 20 rows + rows = extract_rows(cells, start_row=1, end_row=20) + for row in rows: + print(f" Row {row['row']:3d}: ", end="") + items = sorted(row['cells'].items(), + key=lambda x: parse_cell_ref(x[0])) + for ref, val in items[:8]: + val_str = str(val)[:25] + print(f"{ref}={val_str} ", end="") + print() + + +if __name__ == "__main__": + main() diff --git a/promptfoo-evaluation/.security-scan-passed b/promptfoo-evaluation/.security-scan-passed index 6cb5237..5fb6b01 100644 --- a/promptfoo-evaluation/.security-scan-passed +++ b/promptfoo-evaluation/.security-scan-passed @@ -1,4 +1,4 @@ Security scan passed -Scanned at: 2025-12-11T22:24:55.327388 +Scanned at: 2026-03-02T20:00:16.607484 Tool: gitleaks + pattern-based validation -Content hash: d04b93ec8a47fa7b64a2d0ee9790997e5ecc212ddbfa4c2c58fddafa2424d49a +Content hash: 058a48a82477727772269754ab2bae5bb1f575fc264a1e28f1a2cfad25656b95 diff --git a/promptfoo-evaluation/SKILL.md b/promptfoo-evaluation/SKILL.md index 1b9739a..83b2e98 100644 --- a/promptfoo-evaluation/SKILL.md +++ b/promptfoo-evaluation/SKILL.md @@ -440,7 +440,7 @@ tiaogaoren/ └── results/ ``` -**See:** `~/workspace/prompts/tiaogaoren/` for full implementation. +**See:** `./tiaogaoren/` (example project root) for full implementation. ## Resources diff --git a/promptfoo-evaluation/scripts/metrics.py b/promptfoo-evaluation/scripts/metrics.py new file mode 100755 index 0000000..9404e31 --- /dev/null +++ b/promptfoo-evaluation/scripts/metrics.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Reusable assertion helpers for Promptfoo Python checks. + +This module is referenced by examples in promptfoo-evaluation/SKILL.md. +All functions return Promptfoo-compatible result dicts. +""" + + +def _coerce_text(output): + """Normalize Promptfoo output payloads into plain text.""" + if output is None: + return "" + if isinstance(output, str): + return output + if isinstance(output, dict): + # Promptfoo often provides provider response objects. + text = output.get("output") or output.get("content") or "" + if isinstance(text, list): + return "\n".join(str(x) for x in text) + return str(text) + return str(output) + + +def _safe_vars(context): + if isinstance(context, dict): + vars_dict = context.get("vars") + if isinstance(vars_dict, dict): + return vars_dict + return {} + + +def get_assert(output, context): + """Default assertion function used when no function name is provided.""" + text = _coerce_text(output) + vars_dict = _safe_vars(context) + + expected = str(vars_dict.get("expected", "")).strip() + if not expected: + expected = str(vars_dict.get("expected_text", "")).strip() + + if not expected: + return { + "pass": bool(text.strip()), + "score": 1.0 if text.strip() else 0.0, + "reason": "No expected text provided; assertion checks non-empty output.", + "named_scores": {"non_empty": 1.0 if text.strip() else 0.0}, + } + + matched = expected in text + return { + "pass": matched, + "score": 1.0 if matched else 0.0, + "reason": "Output contains expected text." if matched else "Expected text not found.", + "named_scores": {"contains_expected": 1.0 if matched else 0.0}, + } + + +def custom_assert(output, context): + """Alias used by SKILL.md examples.""" + return get_assert(output, context) + + +def custom_check(output, context): + """Check response length against min/max word constraints.""" + text = _coerce_text(output) + vars_dict = _safe_vars(context) + + min_words = int(vars_dict.get("min_words", 100)) + max_words = int(vars_dict.get("max_words", 500)) + words = [w for w in text.split() if w] + count = len(words) + + if count == 0: + return { + "pass": False, + "score": 0.0, + "reason": "Output is empty.", + "named_scores": {"length": 0.0}, + } + + if min_words <= count <= max_words: + return { + "pass": True, + "score": 1.0, + "reason": "Word count within configured range.", + "named_scores": {"length": 1.0}, + } + + if count < min_words: + score = max(0.0, count / float(min_words)) + return { + "pass": False, + "score": round(score, 3), + "reason": "Word count below minimum.", + "named_scores": {"length": round(score, 3)}, + } + + overflow = max(1, count - max_words) + score = max(0.0, 1.0 - (overflow / float(max_words))) + return { + "pass": False, + "score": round(score, 3), + "reason": "Word count above maximum.", + "named_scores": {"length": round(score, 3)}, + } + + +def check_length(output, context): + """Character-length assertion used by advanced examples.""" + text = _coerce_text(output) + vars_dict = _safe_vars(context) + + min_chars = int(vars_dict.get("min_chars", 1)) + max_chars = int(vars_dict.get("max_chars", 3000)) + length = len(text) + + passed = min_chars <= length <= max_chars + if passed: + score = 1.0 + elif length < min_chars: + score = max(0.0, length / float(max(1, min_chars))) + else: + score = max(0.0, max_chars / float(max_chars + (length - max_chars))) + + return { + "pass": passed, + "score": round(score, 3), + "reason": "Character length check.", + "named_scores": {"char_length": round(score, 3)}, + }