xwang 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@
5
5
  "xwang/scripts/xwang-env.sh",
6
6
  "xwang/scripts/xwang-state.sh",
7
7
  "xwang/scripts/xwang-guard.sh",
8
+ "xwang/scripts/xwang-doc-lang-check.sh",
8
9
  "xwang-open/SKILL.md",
9
10
  "xwang-open/scripts/xwang-open-check.sh",
10
11
  "xwang-design/SKILL.md",
@@ -0,0 +1,176 @@
1
+ #!/bin/bash
2
+ # xwang-doc-lang-check — validate language consistency across xwang-generated documents
3
+ # Usage: xwang-doc-lang-check.sh [file-or-dir ...]
4
+ # Exits 0 if all documents are consistent, 1 if any are mixed.
5
+ # On mixed output, stdout contains a correction prompt in the primary language.
6
+
7
+ set -euo pipefail
8
+
9
+ # --- Language detection helpers ---
10
+
11
+ # Count CJK unified ideographs in text
12
+ count_cjk() {
13
+ local text="$1"
14
+ printf '%s' "$text" | perl -CS -ne 'BEGIN { $n=0 } while (/[\x{4e00}-\x{9fff}]/g) { $n++ } END { print $n }'
15
+ }
16
+
17
+ # Count alphabetic ASCII words
18
+ count_words() {
19
+ local text="$1"
20
+ printf '%s' "$text" | grep -oE '[a-zA-Z]+' | wc -l | tr -d ' '
21
+ }
22
+
23
+ # Detect dominant language: "zh" or "en"
24
+ detect_language() {
25
+ local text="$1"
26
+ local cjk words
27
+ cjk=$(count_cjk "$text")
28
+ words=$(count_words "$text")
29
+ if [ "$cjk" -gt "$words" ]; then
30
+ echo "zh"
31
+ else
32
+ echo "en"
33
+ fi
34
+ }
35
+
36
+ # --- Markdown helpers ---
37
+
38
+ is_markdown() {
39
+ local file="$1"
40
+ case "$file" in
41
+ *.md|*.mdx) return 0 ;;
42
+ *) return 1 ;;
43
+ esac
44
+ }
45
+
46
+ # Check a single markdown file for language consistency.
47
+ # Outputs issues to stderr and a correction prompt to stdout when inconsistent.
48
+ check_file() {
49
+ local file="$1"
50
+ local title=""
51
+ local lead=""
52
+ local primary=""
53
+ local section_name=""
54
+ local section_body=""
55
+ local issues=()
56
+
57
+ flush_section() {
58
+ if [ -z "$section_name" ]; then
59
+ return
60
+ fi
61
+ local full_section="${section_name}"$'\n'"${section_body}"
62
+ if [ -z "$primary" ]; then
63
+ lead="${lead}${full_section}"
64
+ primary=$(detect_language "$lead")
65
+ else
66
+ local section_lang
67
+ section_lang=$(detect_language "$full_section")
68
+ if [ "$section_lang" != "$primary" ]; then
69
+ issues+=("section \"$section_name\": $section_lang, expected $primary")
70
+ fi
71
+ fi
72
+ section_name=""
73
+ section_body=""
74
+ }
75
+
76
+ while IFS= read -r line || [ -n "$line" ]; do
77
+ if [[ "$line" =~ ^#[[:space:]] ]]; then
78
+ # Level-1 heading: treat as title / lead context
79
+ title="${line#*# }"
80
+ lead="${lead}${line}"$'\n'
81
+ continue
82
+ fi
83
+
84
+ if [[ "$line" =~ ^##[[:space:]] ]]; then
85
+ flush_section
86
+ section_name="${line##*## }"
87
+ section_body=""
88
+ continue
89
+ fi
90
+
91
+ if [ -n "$section_name" ]; then
92
+ section_body="${section_body}${line}"$'\n'
93
+ else
94
+ # Content before the first section contributes to the lead
95
+ lead="${lead}${line}"$'\n'
96
+ fi
97
+ done < "$file"
98
+
99
+ flush_section
100
+
101
+ # If no sections at all, treat the whole file as one document
102
+ if [ -z "$primary" ]; then
103
+ primary=$(detect_language "$lead")
104
+ fi
105
+
106
+ if [ ${#issues[@]} -eq 0 ]; then
107
+ echo "OK: $file language is consistent ($primary)"
108
+ return 0
109
+ fi
110
+
111
+ echo "MIXED-LANGUAGE: $file (${#issues[@]} issue(s))" >&2
112
+ for issue in "${issues[@]}"; do
113
+ echo " - $issue" >&2
114
+ done
115
+
116
+ if [ "$primary" = "zh" ]; then
117
+ cat <<'EOF'
118
+ 上述文档存在中英文混用问题。项目上下文为中文,请将整份文档统一改写为中文,保持所有章节(包括但不限于标题、Problem Statement、Solution、User Stories、Implementation Decisions、Testing Decisions、Out of Scope、Further Notes、Why、What Changes、Capabilities、Impact 等)语言一致。
119
+ EOF
120
+ else
121
+ cat <<'EOF'
122
+ The document above has mixed languages. The project context is English. Please rewrite the entire document in English, keeping all sections (including but not limited to title, Problem Statement, Solution, User Stories, Implementation Decisions, Testing Decisions, Out of Scope, Further Notes, Why, What Changes, Capabilities, Impact, etc.) consistent in language.
123
+ EOF
124
+ fi
125
+
126
+ return 1
127
+ }
128
+
129
+ # --- Argument collection ---
130
+
131
+ declare -a FILES=()
132
+
133
+ if [ $# -eq 0 ]; then
134
+ echo "Usage: $0 [file-or-dir ...]" >&2
135
+ exit 2
136
+ fi
137
+
138
+ for arg in "$@"; do
139
+ if [ -f "$arg" ]; then
140
+ FILES+=("$arg")
141
+ elif [ -d "$arg" ]; then
142
+ while IFS= read -r f; do
143
+ if is_markdown "$f"; then
144
+ FILES+=("$f")
145
+ fi
146
+ done < <(find "$arg" -type f \( -name '*.md' -o -name '*.mdx' \) \
147
+ ! -path '*/node_modules/*' \
148
+ ! -path '*/.git/*' \
149
+ ! -path '*/dist/*' \
150
+ ! -path '*/.claude/worktrees/*' \
151
+ 2>/dev/null || true)
152
+ else
153
+ echo "WARNING: not found: $arg" >&2
154
+ fi
155
+ done
156
+
157
+ if [ ${#FILES[@]} -eq 0 ]; then
158
+ echo "No markdown documents found to check." >&2
159
+ exit 0
160
+ fi
161
+
162
+ mixed=0
163
+ for file in "${FILES[@]}"; do
164
+ if ! check_file "$file"; then
165
+ mixed=$((mixed + 1))
166
+ fi
167
+ done
168
+
169
+ if [ "$mixed" -eq 0 ]; then
170
+ echo "OK: all ${#FILES[@]} document(s) have consistent language"
171
+ exit 0
172
+ else
173
+ echo "" >&2
174
+ echo "Total: $mixed / ${#FILES[@]} document(s) have mixed languages." >&2
175
+ exit 1
176
+ fi
@@ -65,6 +65,17 @@ if [ -z "${XWANG_GUARD:-}" ]; then
65
65
  fi
66
66
  export XWANG_GUARD
67
67
 
68
+ # Locate xwang-doc-lang-check.sh using the same approach.
69
+ if [ -z "${XWANG_DOC_LANG_CHECK:-}" ]; then
70
+ XWANG_DOC_LANG_CHECK_SIBLING="${XWANG_SCRIPT_DIR}/xwang-doc-lang-check.sh"
71
+ if [ -f "$XWANG_DOC_LANG_CHECK_SIBLING" ]; then
72
+ XWANG_DOC_LANG_CHECK="$(cd "$(dirname "$XWANG_DOC_LANG_CHECK_SIBLING")" && pwd)/$(basename "$XWANG_DOC_LANG_CHECK_SIBLING")"
73
+ else
74
+ XWANG_DOC_LANG_CHECK="$(locate_by_glob '*/xwang/scripts/xwang-doc-lang-check.sh' "${SEARCH_DIRS[@]}" 2>/dev/null || true)"
75
+ fi
76
+ fi
77
+ export XWANG_DOC_LANG_CHECK
78
+
68
79
  if [ -z "$XWANG_OPEN_CHECK" ] || [ ! -f "$XWANG_OPEN_CHECK" ]; then
69
80
  echo "WARNING: xwang-open-check.sh not found" >&2
70
81
  fi
@@ -72,3 +83,7 @@ fi
72
83
  if [ -z "$XWANG_GUARD" ] || [ ! -f "$XWANG_GUARD" ]; then
73
84
  echo "WARNING: xwang-guard.sh not found" >&2
74
85
  fi
86
+
87
+ if [ -z "$XWANG_DOC_LANG_CHECK" ] || [ ! -f "$XWANG_DOC_LANG_CHECK" ]; then
88
+ echo "WARNING: xwang-doc-lang-check.sh not found" >&2
89
+ fi
@@ -13,6 +13,35 @@ red() { echo -e "\033[31m$1\033[0m" >&2; }
13
13
  green() { echo -e "\033[32m$1\033[0m" >&2; }
14
14
  warn() { echo -e "\033[33m$1\033[0m" >&2; }
15
15
 
16
+ # --- Project root resolution ---
17
+
18
+ find_project_root() {
19
+ if [ -n "${XWANG_PROJECT_ROOT:-}" ]; then
20
+ printf '%s' "$XWANG_PROJECT_ROOT"
21
+ return 0
22
+ fi
23
+
24
+ local git_root
25
+ git_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
26
+ if [ -n "$git_root" ]; then
27
+ printf '%s' "$git_root"
28
+ return 0
29
+ fi
30
+
31
+ local dir="$PWD"
32
+ while [ "$dir" != "/" ]; do
33
+ if [ -d "$dir/openspec/changes" ]; then
34
+ printf '%s' "$dir"
35
+ return 0
36
+ fi
37
+ dir=$(dirname "$dir")
38
+ done
39
+
40
+ printf '%s' "$PWD"
41
+ }
42
+
43
+ PROJECT_ROOT=$(find_project_root)
44
+
16
45
  # --- Input validation ---
17
46
 
18
47
  validate_change_name() {
@@ -48,9 +77,16 @@ else
48
77
  if [[ "${3:-}" == "--apply" ]]; then
49
78
  APPLY=1
50
79
  fi
51
- CHANGE_DIR="openspec/changes/$CHANGE"
52
- if [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ]; then
53
- CHANGE_DIR="openspec/changes/archive/$CHANGE"
80
+ CHANGE_DIR="$PROJECT_ROOT/openspec/changes/$CHANGE"
81
+ if [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ]; then
82
+ if [ -d "$PROJECT_ROOT/openspec/changes/archive/$CHANGE" ]; then
83
+ CHANGE_DIR="$PROJECT_ROOT/openspec/changes/archive/$CHANGE"
84
+ elif [ -d "$PROJECT_ROOT/openspec/changes/archive" ]; then
85
+ archived=$(find "$PROJECT_ROOT/openspec/changes/archive" -maxdepth 1 -type d -name "*-$CHANGE" -print -quit 2>/dev/null || true)
86
+ if [ -n "$archived" ]; then
87
+ CHANGE_DIR="$archived"
88
+ fi
89
+ fi
54
90
  fi
55
91
  fi
56
92
 
@@ -306,7 +342,7 @@ branch_status_handled() {
306
342
  design_doc_recorded() {
307
343
  local design_doc
308
344
  design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
309
- if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$design_doc" ]; then
345
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && { [ -f "$PROJECT_ROOT/$design_doc" ] || [ -f "$design_doc" ]; }; then
310
346
  return 0
311
347
  fi
312
348
  echo "design_doc must point to an existing PRD for full workflow before leaving design." >&2
@@ -410,6 +446,7 @@ guard_design() {
410
446
  check "design.md exists and non-empty" file_nonempty "$CHANGE_DIR/design.md"
411
447
  check "tasks.md exists and non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
412
448
  check "tasks.md has at least one task" tasks_has_any
449
+ check "OpenSpec artifacts language consistency" openspec_artifacts_language_consistent
413
450
 
414
451
  if [ "$workflow" = "full" ]; then
415
452
  check "design_doc is recorded for full workflow" design_doc_recorded
@@ -418,7 +455,10 @@ guard_design() {
418
455
  local design_doc
419
456
  design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
420
457
  if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
421
- check "PRD file ($design_doc) exists" file_nonempty "$design_doc"
458
+ local doc_path="$design_doc"
459
+ [ -f "$PROJECT_ROOT/$design_doc" ] && doc_path="$PROJECT_ROOT/$design_doc"
460
+ check "PRD file ($design_doc) exists" file_nonempty "$doc_path"
461
+ check "PRD language consistency" design_doc_language_consistent
422
462
  elif [ "$workflow" != "full" ]; then
423
463
  warn " [WARN] No design_doc recorded in .xwang.yaml (optional for quick/tweak)"
424
464
  fi
@@ -454,6 +494,13 @@ guard_archive() {
454
494
  check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
455
495
  check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
456
496
  check "tasks.md all tasks checked" tasks_all_done
497
+ check "OpenSpec artifacts language consistency" openspec_artifacts_language_consistent
498
+
499
+ local design_doc
500
+ design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
501
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
502
+ check "PRD language consistency" design_doc_language_consistent
503
+ fi
457
504
  }
458
505
 
459
506
  locate_state_script() {
@@ -470,6 +517,66 @@ locate_state_script() {
470
517
  return 1
471
518
  }
472
519
 
520
+ locate_doc_lang_check() {
521
+ local check_sh="$SCRIPT_DIR/xwang-doc-lang-check.sh"
522
+ if [ -f "$check_sh" ]; then
523
+ echo "$check_sh"
524
+ return 0
525
+ fi
526
+ check_sh="$(find "$SCRIPT_DIR" "$SCRIPT_DIR/.." "$HOME" -path '*/xwang/scripts/xwang-doc-lang-check.sh' -type f -print -quit 2>/dev/null || true)"
527
+ if [ -n "$check_sh" ] && [ -f "$check_sh" ]; then
528
+ echo "$check_sh"
529
+ return 0
530
+ fi
531
+ return 1
532
+ }
533
+
534
+ design_doc_language_consistent() {
535
+ local design_doc
536
+ design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
537
+ if [ -z "$design_doc" ] || [ "$design_doc" = "null" ]; then
538
+ return 0
539
+ fi
540
+
541
+ local target=""
542
+ if [ -f "$PROJECT_ROOT/$design_doc" ]; then
543
+ target="$PROJECT_ROOT/$design_doc"
544
+ elif [ -f "$design_doc" ]; then
545
+ target="$design_doc"
546
+ elif [ -f "$CHANGE_DIR/design.md" ]; then
547
+ # design_doc may be stale after archiving; fall back to design.md in the change dir
548
+ target="$CHANGE_DIR/design.md"
549
+ fi
550
+
551
+ if [ -z "$target" ]; then
552
+ echo "design_doc file not found: $design_doc" >&2
553
+ return 1
554
+ fi
555
+
556
+ local check_sh
557
+ if ! check_sh=$(locate_doc_lang_check); then
558
+ echo "xwang-doc-lang-check.sh not found; skipping language check" >&2
559
+ return 0
560
+ fi
561
+ "$XWANG_BASH" "$check_sh" "$target" >/dev/null 2>&1
562
+ }
563
+
564
+ openspec_artifacts_language_consistent() {
565
+ local check_sh
566
+ if ! check_sh=$(locate_doc_lang_check); then
567
+ echo "xwang-doc-lang-check.sh not found; skipping language check" >&2
568
+ return 0
569
+ fi
570
+ local files=()
571
+ for f in "$CHANGE_DIR/proposal.md" "$CHANGE_DIR/design.md" "$CHANGE_DIR/tasks.md"; do
572
+ [ -f "$f" ] && files+=("$f")
573
+ done
574
+ if [ ${#files[@]} -eq 0 ]; then
575
+ return 0
576
+ fi
577
+ "$XWANG_BASH" "$check_sh" "${files[@]}" >/dev/null 2>&1
578
+ }
579
+
473
580
  apply_state_update() {
474
581
  local state_sh
475
582
  if ! state_sh=$(locate_state_script); then
@@ -58,10 +58,18 @@ fi
58
58
  - **暂不归档**:不执行归档,保留当前 `phase: archive` 状态,等待用户稍后再次调用 `/xwang-archive`
59
59
  - [ ] 只有用户选择「确认归档」后才继续 Step 3。
60
60
 
61
- ### Step 3:标记归档状态并校验完整性
61
+ ### Step 3:标记归档状态、校验完整性与 PRD 语言一致性
62
62
 
63
63
  - [ ] 调用 `"$XWANG_STATE" set <name> archived true`,在 change 仍在 `openspec/changes/<name>/` 时把 `.xwang.yaml` 标记为已归档。
64
64
  - [ ] **归档完整性守护**:调用 `"$XWANG_GUARD" <name> archive` 校验归档退出条件(`archived: true`、必要文档存在且 tasks 已勾选)。
65
+ - [ ] **PRD 语言一致性守护**:定位当前 change 对应的 PRD 文件(常见路径如 `docs/mattpocock/<name>-prd.md`、`docs/<name>-prd.md` 或 `<name>-prd.md`)。如存在,运行:
66
+
67
+ ```bash
68
+ "$XWANG_DOC_LANG_CHECK" <prd-file>
69
+ ```
70
+
71
+ 若脚本报告 `MIXED-LANGUAGE`,将其 stdout 中的修正提示词展示给用户,**停止归档**,等待用户修正 PRD 后重新触发 `/xwang-archive`。只有脚本退出 `0` 才继续下一步。
72
+
65
73
  - [ ] 如需,在 PRD 和 plan 文件前置元数据中标注 `archived-with: <name>` 和 `status: archived`。
66
74
 
67
75
  ### Step 4:执行归档
@@ -69,6 +77,7 @@ fi
69
77
  - [ ] 调用 `/openspec-archive-change <name>`(或等价的 OpenSpec archive 命令)按 delta 语义合并主 spec 并移动 change 到归档目录。
70
78
  - [ ] 如归档命令返回非零退出码,报告错误并停止。
71
79
  - [ ] 归档成功后,确认 change 目录已移动到 `openspec/changes/archive/YYYY-MM-DD-<name>/`(或 OpenSpec 默认归档路径)。
80
+ - [ ] **同步 `.xwang.yaml` 中的文件路径**:如果 `design_doc`、`plan`、`verification_report` 等字段指向的是 `openspec/changes/<name>/` 下的旧路径,更新为归档目录 `openspec/changes/archive/YYYY-MM-DD-<name>/` 下的相对路径,避免后续 guard 或读取时路径失效。
72
81
  - [ ] 向用户汇报归档完成、新目录路径与生命周期闭环状态。
73
82
 
74
83
  ## 退出条件
@@ -105,8 +105,18 @@ grilling 确认完成后、生成 PRD 前,**必须**执行 PRD 拆分预检并
105
105
 
106
106
  ### Step 2:生成 PRD
107
107
 
108
- - [ ] 用户确认后,调用 `/to-prd` 生成**中文** PRD(遵循 to-prd 的 PRD 模板),无需额外用户确认。
108
+ - [ ] 用户确认后,调用 `/to-prd` 生成 PRD(遵循 to-prd 的 PRD 模板),无需额外用户确认。
109
109
  - [ ] 将 PRD 写入 `docs/mattpocock/<name>-prd.md`;如果 grilling 过程中名称有调整,使用最终生成的名称。
110
+ - [ ] **PRD 语言一致性守护**:写入后运行语言检测脚本:
111
+
112
+ ```bash
113
+ XWANG_ENV="${XWANG_ENV:-$(find . "$HOME"/.*/skills "$HOME/.config" -path '*/xwang/scripts/xwang-env.sh' -type f -print -quit 2>/dev/null)}"
114
+ . "$XWANG_ENV"
115
+ "$XWANG_DOC_LANG_CHECK" docs/mattpocock/<name>-prd.md
116
+ ```
117
+
118
+ 若脚本报告 `MIXED-LANGUAGE`,将其 stdout 中的修正提示词追加到下一次 `/to-prd` 请求中,重新生成并写入 PRD,重复检测直到脚本退出 `0`。
119
+
110
120
  - [ ] **如果 Step 1a 结论为「保持为一个 change」,在 PRD 中显式写入“不拆分原因”章节。**
111
121
 
112
122
  ### Step 3:创建 OpenSpec change
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xwang",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "xwang CLI",
5
5
  "keywords": [
6
6
  "xwang",