Add Nexa BaZi naming contract checker
This commit is contained in:
@@ -2,3 +2,4 @@ __pycache__/
|
||||
*.py[cod]
|
||||
.DS_Store
|
||||
*.pdf
|
||||
nexa_bazi-main/
|
||||
|
||||
@@ -10,6 +10,8 @@ The skill is simplified-Chinese-first:
|
||||
|
||||
The source PDF is not included in this repository.
|
||||
|
||||
This repository also includes a Nexa BaZi contract checker for programmers. It catches mixed API fields, JSON schema names, prompt variables, and module keys such as `Day_Master`, `DayMaster_Stem`, `Dominant_Deity`, and `Primary_Favorite_Element`.
|
||||
|
||||
## Install
|
||||
|
||||
Clone the repository and copy it into Codex skills:
|
||||
@@ -44,7 +46,13 @@ Use $bazi-translation to translate these terms into approved English and code na
|
||||
Use $bazi-translation to review API field names in packages/bazi.
|
||||
```
|
||||
|
||||
## Use The Checker
|
||||
For Nexa:
|
||||
|
||||
```text
|
||||
Use $bazi-translation to review Nexa BaZi API fields, prompt variables, and module keys in nexa_bazi-main.
|
||||
```
|
||||
|
||||
## Use The Generic Checker
|
||||
|
||||
Run the local checker against files or directories:
|
||||
|
||||
@@ -72,6 +80,47 @@ src/chart.ts:42: dayOwner -> dayMaster
|
||||
line: const dayOwner = chart.dayOwner;
|
||||
```
|
||||
|
||||
## Use The Nexa Contract Checker
|
||||
|
||||
Run the Nexa-specific checker against code, JSON, prompt files, or docs:
|
||||
|
||||
```bash
|
||||
python ~/.codex/skills/bazi-translation/scripts/check_nexa_bazi_contract.py <path>
|
||||
```
|
||||
|
||||
Migration planning mode:
|
||||
|
||||
```bash
|
||||
python ~/.codex/skills/bazi-translation/scripts/check_nexa_bazi_contract.py <path> --no-fail
|
||||
```
|
||||
|
||||
CI-style blocking mode, reporting only hard errors:
|
||||
|
||||
```bash
|
||||
python ~/.codex/skills/bazi-translation/scripts/check_nexa_bazi_contract.py <path> --min-severity error
|
||||
```
|
||||
|
||||
List loaded Nexa rules:
|
||||
|
||||
```bash
|
||||
python ~/.codex/skills/bazi-translation/scripts/check_nexa_bazi_contract.py --list-rules
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```text
|
||||
5 八字详解/5.7 命理符号/2.命理符号-算法与判定规则.md:14: DayMaster_Stem -> day_master_stem [error]
|
||||
concept: 日主天干
|
||||
reason: mixed case 不适合接口契约
|
||||
line: DayMaster_Stem: "辛"
|
||||
```
|
||||
|
||||
Severity handling:
|
||||
|
||||
- `error`: do not add in new code; rename or add a compatibility migration.
|
||||
- `warn`: likely wrong for new BaZi fields; check whether it is already published.
|
||||
- `review`: not automatically unsafe, but it may show cross-system leakage or a published key that needs migration planning.
|
||||
|
||||
## Core Naming Rules
|
||||
|
||||
- Use `dayMaster`, not `dayOwner` or `dayLord`.
|
||||
@@ -81,11 +130,24 @@ src/chart.ts:42: dayOwner -> dayMaster
|
||||
- Use `usefulGod`, not `useGod`.
|
||||
- Use `favorableElement` in code by default; keep `Favourable Element` for book-style display text.
|
||||
- Keep `Useful God` separate from `Favourable Element`.
|
||||
- For Nexa JSON/API/prompt fields, use `snake_case`: `day_master`, `day_master_stem`, `dominant_ten_god`, `primary_favorite_element`.
|
||||
- Do not use `deity` for Ten Gods in interfaces. Use `ten_god`.
|
||||
- Do not use astrology terms such as `astral_event` for BaZi branch relationships or daily BaZi events.
|
||||
|
||||
## Nexa Contract Files
|
||||
|
||||
- `references/nexa-bazi-contract.md`: canonical Nexa BaZi API/JSON/prompt naming contract.
|
||||
- `references/nexa-naming-rules.md`: practical naming rules and migration guidance.
|
||||
- `references/nexa-forbidden-names.md`: deterministic bad-name table used by the checker.
|
||||
|
||||
## Files
|
||||
|
||||
- `SKILL.md`: Codex skill trigger and workflow.
|
||||
- `references/terms.md`: simplified-Chinese-first dictionary.
|
||||
- `references/naming-rules.md`: review rules and anti-patterns.
|
||||
- `references/nexa-bazi-contract.md`: Nexa BaZi interface and prompt-variable naming contract.
|
||||
- `references/nexa-naming-rules.md`: Nexa-specific naming guidance.
|
||||
- `references/nexa-forbidden-names.md`: Nexa forbidden/legacy name table.
|
||||
- `references/pending-terms.md`: staging area for terms needing domain review.
|
||||
- `scripts/check_bazi_names.py`: source-code naming checker.
|
||||
- `scripts/check_nexa_bazi_contract.py`: Nexa BaZi API/JSON/prompt naming checker.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: bazi-translation
|
||||
description: Simplified-Chinese-first BaZi/Four Pillars terminology dictionary and code naming review workflow. Use when translating Chinese BaZi, Four Pillars, Chinese metaphysics, destiny analysis, Heavenly Stem/Earthly Branch, Ten Gods, Five Factors, luck cycle, or favorable-element terms into English; naming code identifiers, classes, files, API fields, database columns, UI labels, or documentation; or reviewing code for inconsistent or literal translations such as dayOwner, skyStem, groundBranch, fortuneColumn, useGod, or mixed BaZi vocabulary.
|
||||
description: Simplified-Chinese-first BaZi/Four Pillars terminology dictionary and deterministic code/interface naming review workflow, including a Nexa BaZi system contract. Use when translating Chinese BaZi, Four Pillars, Chinese metaphysics, destiny analysis, Heavenly Stem/Earthly Branch, Ten Gods, Five Factors, luck cycle, or favorable-element terms into English; naming code identifiers, classes, files, API fields, database columns, JSON schema fields, prompt variables, UI labels, or documentation; or reviewing code for inconsistent or literal translations such as dayOwner, skyStem, groundBranch, fortuneColumn, useGod, Day_Master, Dominant_Deity, Primary_Favorite_Element, or mixed BaZi vocabulary.
|
||||
---
|
||||
|
||||
# BaZi Translation
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill to keep BaZi terminology readable and consistent across code, UI labels, comments, docs, schemas, and tests.
|
||||
Use this skill to keep BaZi terminology readable and consistent across code, UI labels, comments, docs, schemas, prompt variables, and tests.
|
||||
|
||||
The source of truth is simplified Chinese first. Use the approved English display term for user-facing text, and use the approved ASCII code name for identifiers unless an existing project convention is stronger.
|
||||
|
||||
@@ -23,8 +23,16 @@ python ~/.codex/skills/bazi-translation/scripts/check_bazi_names.py <path>
|
||||
|
||||
Use `--no-fail` for exploratory review and `--list-approved` to print approved code names.
|
||||
|
||||
4. Report naming findings as concrete replacements: current name, approved name, reason, and file/line when available.
|
||||
5. When a concept is not in `terms.md`, do not invent a final translation. Add or propose a row in `references/pending-terms.md` with context and a provisional English/code name.
|
||||
4. For Nexa BaZi code, APIs, JSON schemas, prompt variables, and module keys, read `references/nexa-bazi-contract.md`, `references/nexa-naming-rules.md`, and `references/nexa-forbidden-names.md`, then run:
|
||||
|
||||
```bash
|
||||
python ~/.codex/skills/bazi-translation/scripts/check_nexa_bazi_contract.py <path>
|
||||
```
|
||||
|
||||
Use `--no-fail` during migration planning and `--min-severity error` for CI-style blocking checks.
|
||||
|
||||
5. Report naming findings as concrete replacements: current name, approved name, reason, severity, and file/line when available.
|
||||
6. When a concept is not in `terms.md` or the Nexa contract, do not invent a final translation. Add or propose a row in `references/pending-terms.md` with context and a provisional English/code name.
|
||||
|
||||
## Standards
|
||||
|
||||
@@ -35,10 +43,16 @@ Use `--no-fail` for exploratory review and `--list-approved` to print approved c
|
||||
- Keep Chinese, English display text, and code names separate. Do not force one field to serve all audiences.
|
||||
- Do not conflate `用神` (`Useful God`, advanced concept) with `喜用元素` or `有利元素` (`Favourable Element`, beginner balancing element).
|
||||
- Do not translate classical terms word by word when the approved table has a domain term.
|
||||
- For programmer-facing Nexa contracts, prefer deterministic rules over vector/semantic matching. Vector search may help discover candidates, but it must not decide final names.
|
||||
- In Nexa JSON/API fields, prefer `snake_case`; reserve PascalCase for type names and camelCase for language-local code when the host language expects it.
|
||||
|
||||
## Resources
|
||||
|
||||
- `references/terms.md`: approved simplified-Chinese-first dictionary.
|
||||
- `references/naming-rules.md`: code naming rules, anti-patterns, and review checklist.
|
||||
- `references/nexa-bazi-contract.md`: Nexa BaZi interface and prompt-variable naming contract.
|
||||
- `references/nexa-naming-rules.md`: Nexa-specific naming rules for APIs, JSON, module keys, and prompt files.
|
||||
- `references/nexa-forbidden-names.md`: deterministic legacy/bad-name table used by the Nexa checker.
|
||||
- `references/pending-terms.md`: staging area for terms needing domain review.
|
||||
- `scripts/check_bazi_names.py`: scans source files for forbidden or suspicious BaZi translations and suggests approved names.
|
||||
- `scripts/check_nexa_bazi_contract.py`: scans Nexa code/docs for mixed interface names and suggests contract names.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Nexa 八字命名契约
|
||||
|
||||
本文件是 Nexa 八字系统的程序员命名契约。目标不是翻译给用户看,而是让 API、JSON、prompt 变量、模块 key、测试数据和文档中的八字命理词汇保持一致。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- Provider / BFF / frontend API 字段。
|
||||
- JSON schema、测试 fixture、调试数据。
|
||||
- Prompt 变量、Context Vault、fallback schema。
|
||||
- 模块 key、SSE path、registry key。
|
||||
- 代码中的函数、变量、类名和枚举名。
|
||||
|
||||
## 分层原则
|
||||
|
||||
| 层级 | 命名风格 | 示例 | 说明 |
|
||||
|---|---|---|---|
|
||||
| API / JSON 字段 | `snake_case` | `day_master`, `ten_god_label` | 对外和跨语言最稳定 |
|
||||
| 模块 key / registry key | `snake_case` | `daily_overview`, `monthly_luck_tag` | 已发布 key 需要迁移策略 |
|
||||
| Python 字段/变量 | `snake_case` | `day_master_stem` | 跟 Python 社区一致 |
|
||||
| JS/TS 局部变量 | `camelCase` | `dayMasterStem` | 仅限局部代码,不用于 JSON contract |
|
||||
| Type / Class / Enum | `PascalCase` | `HeavenlyStem`, `EarthlyBranch`, `TenGod` | 类型名 |
|
||||
| UI 中文 | 简体中文 | `日主`, `十神`, `流日` | 面向用户 |
|
||||
| 英文显示名 | 标准术语 | `Day Master`, `Ten Gods`, `Annual Luck` | 面向英文 UI/文档 |
|
||||
|
||||
## Nexa 推荐字段
|
||||
|
||||
| 中文概念 | 推荐 API/JSON 字段 | Type/Enum | 说明 |
|
||||
|---|---|---|---|
|
||||
| 八字命盘 | `bazi_chart` | `BaziChart` | 完整排盘结构 |
|
||||
| 四柱 | `pillars` | `Pillars` | 包含 year/month/day/hour |
|
||||
| 年柱 | `year_pillar` | `Pillar` | 不要写 `year_column` |
|
||||
| 月柱 | `month_pillar` | `Pillar` | 不要写 `month_column` |
|
||||
| 日柱 | `day_pillar` | `Pillar` | 不要写 `day_column` |
|
||||
| 时柱 | `hour_pillar` | `Pillar` | 不要写 `time_column` |
|
||||
| 日主 | `day_master` | `DayMaster` | 可表示完整日主,如 `辛金` |
|
||||
| 日主天干 | `day_master_stem` | `HeavenlyStem` | 只表示天干,如 `辛` |
|
||||
| 天干 | `heavenly_stem` | `HeavenlyStem` | JSON 字段用 snake_case |
|
||||
| 地支 | `earthly_branch` | `EarthlyBranch` | JSON 字段用 snake_case |
|
||||
| 藏干 | `hidden_stems` | `HiddenStem` | list 字段用复数 |
|
||||
| 十神 | `ten_god` | `TenGod` | 单个十神 |
|
||||
| 十神 ID | `ten_god_label` | `TenGodLabel` | 若已上线为数字 ID,可保留但要写清映射 |
|
||||
| 主导十神 | `dominant_ten_god` | `TenGod` | 不要写 `dominant_deity` |
|
||||
| 五行 | `element` / `five_elements` | `FiveElement` | 单个用 `element`,集合用 `five_elements` |
|
||||
| 五行强度 | `element_strengths` | `ElementStrengths` | map: wood/fire/earth/metal/water |
|
||||
| 喜用元素 | `favorite_element` | `FiveElement` | Nexa 内部可用 favorite;若对齐通用词表可映射为 favorable |
|
||||
| 第一喜用元素 | `primary_favorite_element` | `FiveElement` | 不要写 `Primary_Favorite_Element` |
|
||||
| 第二喜用元素 | `secondary_favorite_element` | `FiveElement` | 不要写 `Secondary_Favorite_Element` |
|
||||
| 用神 | `useful_god` | `UsefulGod` | 高阶概念,不等同喜用元素 |
|
||||
| 大运 | `luck_pillar` | `LuckPillar` | 十年运柱 |
|
||||
| 流年 | `annual_luck` | `AnnualLuck` | 避免和本命年柱混淆 |
|
||||
| 流月 | `monthly_luck` | `MonthlyLuck` | 月运 |
|
||||
| 流日 | `daily_luck` | `DailyLuck` | 日运 |
|
||||
| 流日干支 | `flow_day_ganzhi` | `StemBranch` | 若已上线可保留;新字段建议 `daily_stem_branch` |
|
||||
| 合冲刑害破 | `branch_relationship` | `BranchRelationship` | 不要混用 astrology/astral 命名 |
|
||||
| 合盘 | `bazi_synastry` | `BaziSynastry` | 与占星 synastry 区分时加 `bazi_` 前缀 |
|
||||
|
||||
## 已发布字段处理
|
||||
|
||||
已发布 API 或前端依赖字段不能无迁移直接改名。处理顺序:
|
||||
|
||||
1. 标记为 `compat`:旧字段继续读取。
|
||||
2. 新增推荐字段:例如同时返回 `day_master_stem`。
|
||||
3. 在文档中声明旧字段 deprecated。
|
||||
4. 前端/BFF 迁移完成后,再删除旧字段。
|
||||
|
||||
审查时不要只说“改名”。要说明影响面:API、fixture、prompt、前端 path、SSE key、数据库列、测试断言。
|
||||
|
||||
## 不做向量判断
|
||||
|
||||
本契约用于程序员纠错,必须可重复、可解释、可进入 CI。不要用向量相似度决定字段是否违规。向量搜索最多用于发现候选混名,最终仍必须落回本契约或 `nexa-forbidden-names.md`。
|
||||
@@ -0,0 +1,38 @@
|
||||
# Nexa 禁用/兼容命名表
|
||||
|
||||
本表供 `scripts/check_nexa_bazi_contract.py` 读取。`error` 表示新代码不应出现;`warn` 表示需要迁移或人工确认;`review` 表示可能是已发布兼容字段,改动前必须查影响面。
|
||||
|
||||
| severity | current | preferred | concept | reason |
|
||||
|---|---|---|---|---|
|
||||
| error | Day_Master | day_master | 日主 | Nexa JSON/API/prompt 字段使用 snake_case |
|
||||
| error | DayMaster | day_master | 日主 | JSON/API 字段不要用 PascalCase 或拼接式 camel |
|
||||
| error | DayMaster_Stem | day_master_stem | 日主天干 | mixed case 不适合接口契约 |
|
||||
| error | dayMasterStem | day_master_stem | 日主天干 | JSON/API 字段使用 snake_case;JS 局部变量除外 |
|
||||
| error | Primary_Favorite_Element | primary_favorite_element | 第一喜用元素 | mixed case 不适合接口契约 |
|
||||
| error | Secondary_Favorite_Element | secondary_favorite_element | 第二喜用元素 | mixed case 不适合接口契约 |
|
||||
| error | Favorite_Element | favorite_element | 喜用元素 | mixed case 不适合接口契约 |
|
||||
| error | Element_Strengths | element_strengths | 五行强度 | mixed case 不适合接口契约 |
|
||||
| error | Dominant_Deity | dominant_ten_god | 主导十神 | 十神不是 deity |
|
||||
| error | dominant_deity | dominant_ten_god | 主导十神 | 十神不是 deity |
|
||||
| error | deity | ten_god | 十神 | 接口字段不要用 deity 表示十神 |
|
||||
| error | useGod | useful_god | 用神 | 不要把 Useful God 写成动词短语 |
|
||||
| error | usefulGod | useful_god | 用神 | Nexa JSON/API 字段使用 snake_case |
|
||||
| error | primary_useful_god | primary_favorite_element | 喜用元素 | 喜用元素不等同用神 |
|
||||
| error | dayOwner | day_master | 日主 | 日主标准术语是 Day Master |
|
||||
| error | dayLord | day_master | 日主 | 日主标准术语是 Day Master |
|
||||
| error | skyStem | heavenly_stem | 天干 | 标准术语是 Heavenly Stem |
|
||||
| error | groundBranch | earthly_branch | 地支 | 标准术语是 Earthly Branch |
|
||||
| error | fortuneColumn | luck_pillar | 大运 | 大运是 Luck Pillar,不是 column |
|
||||
| error | fortune_column | luck_pillar | 大运 | 大运是 Luck Pillar,不是 column |
|
||||
| error | big_luck | luck_pillar | 大运 | 新接口使用 luck_pillar |
|
||||
| error | annual_pillar | annual_luck | 流年 | 避免和本命 year_pillar 混淆 |
|
||||
| error | monthly_pillar | monthly_luck | 流月 | 避免和本命 month_pillar 混淆 |
|
||||
| error | daily_pillar | daily_luck | 流日 | 避免和本命 day_pillar 混淆 |
|
||||
| warn | flowing_year | annual_luck | 流年 | 直译味过重,优先 annual_luck |
|
||||
| warn | flowing_month | monthly_luck | 流月 | 直译味过重,优先 monthly_luck |
|
||||
| warn | flowing_day | daily_luck | 流日 | 若表示干支请用 daily_stem_branch |
|
||||
| warn | daily_fortune_insights | daily_luck_insights | 日运洞察模块 | 新八字模块 key 优先用 luck;已发布则兼容迁移 |
|
||||
| review | key_astral_event | key_bazi_event | 今日关键八字事件 | 八字模块避免 astral;若已发布需迁移 |
|
||||
| review | astral_event | bazi_event | 八字事件 | astral 属占星语境,八字事实链不要混用 |
|
||||
| review | provider_visual_tags | provider_bazi_tags | Provider 标签 | 若标签全是八字来源,名称应体现 bazi |
|
||||
| review | provider_visual_tag_detail | provider_bazi_tag_detail | Provider 标签详情 | 若标签全是八字来源,名称应体现 bazi |
|
||||
@@ -0,0 +1,78 @@
|
||||
# Nexa 八字程序员命名规则
|
||||
|
||||
## 最高优先级
|
||||
|
||||
1. JSON/API 字段使用 `snake_case`。
|
||||
2. 八字体系字段不得混入占星术语,除非该模块确实属于占星产品线。
|
||||
3. 十神不要翻译成 `deity`、`god`、`star` 作为接口字段。对外字段用 `ten_god`,用户文案可写中文十神名。
|
||||
4. `favorite_element` 是 Nexa 喜用元素字段;`useful_god` 是用神字段。两者不要互换。
|
||||
5. 已上线字段必须走兼容迁移,不做无提示破坏性重命名。
|
||||
|
||||
## 推荐命名
|
||||
|
||||
| 类别 | 推荐 | 避免 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 日主 | `day_master` | `Day_Master`, `dayOwner`, `self_lord` | API/JSON 用 snake_case |
|
||||
| 日主天干 | `day_master_stem` | `DayMaster_Stem`, `dayMasterStem` in JSON | JSON 禁止 mixed case |
|
||||
| 主导十神 | `dominant_ten_god` | `Dominant_Deity`, `dominant_star` | 十神不是 deity |
|
||||
| 喜用元素 | `primary_favorite_element` | `Primary_Favorite_Element`, `primary_useful_god` | 喜用元素不等同用神 |
|
||||
| 五行强度 | `element_strengths` | `Element_Strengths`, `energy_strengths` | 明确是五行强度 |
|
||||
| 大运 | `luck_pillar` | `fortune_column`, `big_luck` | 八字术语是 Luck Pillar |
|
||||
| 流年 | `annual_luck` | `flowing_year`, `annual_pillar` | 避免和本命年柱混淆 |
|
||||
| 流日 | `daily_luck` | `flow_day` when used as event concept | 若表示干支可写 `daily_stem_branch` |
|
||||
| 刑冲合害 | `branch_relationship` | `astral_event` | 八字地支关系不是占星事件 |
|
||||
|
||||
## 模块 key
|
||||
|
||||
模块 key 是前后端契约,不是标题文案。命名要表达数据职责:
|
||||
|
||||
| 当前/候选 | 建议 | 备注 |
|
||||
|---|---|---|
|
||||
| `daily_overview` | 保留 | 已清晰,非术语问题 |
|
||||
| `monthly_luck_tag` | 保留 | 已上线可保留 |
|
||||
| `daily_luck_mantra` | 保留 | 产品文案 key,可保留 |
|
||||
| `daily_fortune_insights` | `daily_luck_insights` | 新增模块建议用 luck;已上线则兼容迁移 |
|
||||
| `key_astral_event` | `key_bazi_event` 或 `key_energy_event` | 八字首页避免 astral |
|
||||
| `home_image_module` | 保留或改为业务中性名 | 不属于术语违规 |
|
||||
|
||||
## Prompt 变量
|
||||
|
||||
Prompt 变量也属于接口契约,不应出现临时写法:
|
||||
|
||||
```yaml
|
||||
destiny_symbols_context:
|
||||
birth_date: "1999-09-09"
|
||||
day_master_stem: "辛"
|
||||
primary_favorite_element: "金"
|
||||
secondary_favorite_element: "水"
|
||||
element_strengths:
|
||||
wood: 10
|
||||
fire: 35
|
||||
earth: 12
|
||||
metal: 22
|
||||
water: 8
|
||||
```
|
||||
|
||||
不要写:
|
||||
|
||||
```yaml
|
||||
DayMaster_Stem: "辛"
|
||||
Primary_Favorite_Element: "金"
|
||||
Dominant_Deity: "七杀"
|
||||
```
|
||||
|
||||
## 审查输出
|
||||
|
||||
输出必须具体到替换建议:
|
||||
|
||||
```text
|
||||
path/file.md:14 DayMaster_Stem -> day_master_stem [error]
|
||||
原因:Nexa JSON/prompt contract 使用 snake_case;日主天干字段为 day_master_stem。
|
||||
```
|
||||
|
||||
如果是已发布字段,输出迁移提醒:
|
||||
|
||||
```text
|
||||
key_astral_event -> key_bazi_event [review]
|
||||
原因:八字模块不应使用 astral;若前端已依赖,先新增新 key 并保留旧 key 兼容。
|
||||
```
|
||||
Executable
+247
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan Nexa BaZi files for mixed interface and prompt-variable names."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_RULES = SKILL_DIR / "references" / "nexa-forbidden-names.md"
|
||||
|
||||
SEVERITY_ORDER = {"review": 0, "warn": 1, "error": 2}
|
||||
|
||||
EXCLUDE_DIRS = {
|
||||
".git",
|
||||
".hg",
|
||||
".svn",
|
||||
".idea",
|
||||
".vscode",
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
"vendor",
|
||||
"dist",
|
||||
"build",
|
||||
"coverage",
|
||||
".next",
|
||||
".nuxt",
|
||||
".venv",
|
||||
"venv",
|
||||
}
|
||||
|
||||
TEXT_EXTENSIONS = {
|
||||
".c",
|
||||
".cc",
|
||||
".cpp",
|
||||
".cs",
|
||||
".css",
|
||||
".csv",
|
||||
".go",
|
||||
".h",
|
||||
".hpp",
|
||||
".html",
|
||||
".java",
|
||||
".js",
|
||||
".json",
|
||||
".jsx",
|
||||
".kt",
|
||||
".lua",
|
||||
".md",
|
||||
".mjs",
|
||||
".php",
|
||||
".py",
|
||||
".rb",
|
||||
".rs",
|
||||
".scss",
|
||||
".sh",
|
||||
".sql",
|
||||
".svelte",
|
||||
".swift",
|
||||
".toml",
|
||||
".ts",
|
||||
".tsx",
|
||||
".txt",
|
||||
".vue",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Rule:
|
||||
severity: str
|
||||
current: str
|
||||
preferred: str
|
||||
concept: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Issue:
|
||||
path: Path
|
||||
line_no: int
|
||||
line: str
|
||||
rule: Rule
|
||||
|
||||
|
||||
def split_md_row(line: str) -> list[str]:
|
||||
stripped = line.strip()
|
||||
if not stripped.startswith("|") or not stripped.endswith("|"):
|
||||
return []
|
||||
return [cell.strip().strip("`").strip() for cell in stripped.strip("|").split("|")]
|
||||
|
||||
|
||||
def parse_rules(path: Path) -> list[Rule]:
|
||||
header: list[str] | None = None
|
||||
rules: list[Rule] = []
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
cells = split_md_row(raw_line)
|
||||
if not cells:
|
||||
continue
|
||||
if {"severity", "current", "preferred"}.issubset(set(cells)):
|
||||
header = cells
|
||||
continue
|
||||
if cells and all(set(cell) <= {"-"} for cell in cells):
|
||||
continue
|
||||
if not header or len(cells) < len(header):
|
||||
continue
|
||||
|
||||
row = dict(zip(header, cells))
|
||||
severity = row.get("severity", "").lower()
|
||||
if severity not in SEVERITY_ORDER:
|
||||
continue
|
||||
current = row.get("current", "").strip()
|
||||
preferred = row.get("preferred", "").strip()
|
||||
if not current or not preferred:
|
||||
continue
|
||||
rules.append(
|
||||
Rule(
|
||||
severity=severity,
|
||||
current=current,
|
||||
preferred=preferred,
|
||||
concept=row.get("concept", ""),
|
||||
reason=row.get("reason", ""),
|
||||
)
|
||||
)
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def iter_files(paths: list[Path]) -> list[Path]:
|
||||
found: list[Path] = []
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
print(f"warning: path does not exist: {path}", file=sys.stderr)
|
||||
continue
|
||||
if path.is_file():
|
||||
if is_text_candidate(path):
|
||||
found.append(path)
|
||||
continue
|
||||
for root, dirs, files in os.walk(path):
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
for name in files:
|
||||
candidate = Path(root) / name
|
||||
if is_text_candidate(candidate):
|
||||
found.append(candidate)
|
||||
return found
|
||||
|
||||
|
||||
def is_text_candidate(path: Path) -> bool:
|
||||
return path.suffix.lower() in TEXT_EXTENSIONS
|
||||
|
||||
|
||||
def identifier_pattern(name: str) -> re.Pattern[str] | None:
|
||||
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
|
||||
return None
|
||||
return re.compile(r"(?<![A-Za-z0-9_])" + re.escape(name) + r"(?![A-Za-z0-9_])")
|
||||
|
||||
|
||||
def line_has_rule(line: str, rule: Rule) -> bool:
|
||||
pattern = identifier_pattern(rule.current)
|
||||
if pattern:
|
||||
return bool(pattern.search(line))
|
||||
return rule.current in line
|
||||
|
||||
|
||||
def scan(paths: list[Path], rules: list[Rule], min_severity: str) -> list[Issue]:
|
||||
min_rank = SEVERITY_ORDER[min_severity]
|
||||
active_rules = [rule for rule in rules if SEVERITY_ORDER[rule.severity] >= min_rank]
|
||||
issues: list[Issue] = []
|
||||
|
||||
for path in iter_files(paths):
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||
for rule in active_rules:
|
||||
if line_has_rule(line, rule):
|
||||
issues.append(Issue(path=path, line_no=line_no, line=line.strip(), rule=rule))
|
||||
return issues
|
||||
|
||||
|
||||
def print_rules(rules: list[Rule]) -> None:
|
||||
for rule in rules:
|
||||
print(f"{rule.severity}\t{rule.current}\t{rule.preferred}\t{rule.concept}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("paths", nargs="*", type=Path, help="files or directories to scan")
|
||||
parser.add_argument("--rules", type=Path, default=DEFAULT_RULES, help="path to nexa-forbidden-names.md")
|
||||
parser.add_argument(
|
||||
"--min-severity",
|
||||
choices=sorted(SEVERITY_ORDER, key=SEVERITY_ORDER.get),
|
||||
default="review",
|
||||
help="minimum severity to report",
|
||||
)
|
||||
parser.add_argument("--list-rules", action="store_true", help="print loaded rules and exit")
|
||||
parser.add_argument("--no-fail", action="store_true", help="always exit 0")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.rules.exists():
|
||||
print(f"rules file not found: {args.rules}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
rules = parse_rules(args.rules)
|
||||
if args.list_rules:
|
||||
print_rules(rules)
|
||||
return 0
|
||||
|
||||
if not args.paths:
|
||||
parser.error("provide at least one file or directory")
|
||||
|
||||
issues = scan(args.paths, rules, args.min_severity)
|
||||
if not issues:
|
||||
print("No Nexa BaZi contract naming issues found.")
|
||||
return 0
|
||||
|
||||
for issue in issues:
|
||||
rule = issue.rule
|
||||
print(f"{issue.path}:{issue.line_no}: {rule.current} -> {rule.preferred} [{rule.severity}]")
|
||||
print(f" concept: {rule.concept}")
|
||||
print(f" reason: {rule.reason}")
|
||||
print(f" line: {issue.line}")
|
||||
|
||||
counts: dict[str, int] = {"error": 0, "warn": 0, "review": 0}
|
||||
for issue in issues:
|
||||
counts[issue.rule.severity] += 1
|
||||
print(f"\n{len(issues)} issue(s) found: {counts['error']} error, {counts['warn']} warn, {counts['review']} review.")
|
||||
|
||||
blocking = any(issue.rule.severity in {"error", "warn"} for issue in issues)
|
||||
return 0 if args.no_fail or not blocking else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user