kcode-pi 0.1.5 → 0.1.7

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.
Files changed (50) hide show
  1. package/README.md +35 -2
  2. package/dist/cli/kcode.d.ts +1 -0
  3. package/dist/cli/kcode.js +27 -4
  4. package/package.json +1 -1
  5. package/src/cli/kcode.ts +29 -4
  6. package/src/official/kingdee-skills.ts +60 -13
  7. package/src/rules/checker.ts +143 -0
  8. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +2 -2
  9. package/vendor/kingdee-skills/ok-cosmic/SKILL.md +52 -101
  10. package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +4 -4
  11. package/vendor/kingdee-skills/ok-cosmic/manifest.json +21 -20
  12. package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +1 -1
  13. package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +1 -1
  14. package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +2 -2
  15. package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +4 -4
  16. package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +3 -3
  17. package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +8 -8
  18. package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +1 -1
  19. package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +19 -18
  20. package/vendor/kingdee-skills/ok-ksql/SKILL.md +9 -9
  21. package/vendor/kingdee-skills/ok-ksql/manifest.json +2 -1
  22. package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +2 -2
  23. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +0 -336
  24. package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +0 -121
  25. package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +0 -295
  26. package/vendor/kingdee-skills/ok-cosmic/README.md +0 -460
  27. package/vendor/kingdee-skills/ok-cosmic/requirements.txt +0 -2
  28. package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +0 -204
  29. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +0 -910
  30. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +0 -359
  31. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +0 -181
  32. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +0 -389
  33. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +0 -856
  34. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +0 -262
  35. package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +0 -293
  36. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +0 -2
  37. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +0 -393
  38. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +0 -176
  39. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +0 -375
  40. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +0 -434
  41. package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +0 -36
  42. package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +0 -186
  43. package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +0 -40
  44. package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +0 -142
  45. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
  46. package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
  47. package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +0 -18
  48. package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +0 -53
  49. package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
  50. package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +0 -363
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: NOASSERTION
3
- """Small CLI helpers shared by ok-cosmic scripts."""
4
-
5
- from __future__ import annotations
6
-
7
- import argparse
8
- import sys
9
- from typing import Callable, Optional
10
-
11
-
12
- class FriendlyArgumentParser(argparse.ArgumentParser):
13
- """ArgumentParser that reports concise failures instead of dumping usage."""
14
-
15
- def error(self, message: str) -> None:
16
- self.exit(1, f"✖️ 参数错误: {message}\n提示: 使用 --help 查看参数。\n")
17
-
18
-
19
- def run_cli(main_func: Callable[[], Optional[int]]) -> int:
20
- """Run a CLI entrypoint and convert uncaught exceptions to failure text."""
21
-
22
- try:
23
- result = main_func()
24
- if isinstance(result, int):
25
- return result
26
- return 0
27
- except SystemExit as e:
28
- code = e.code
29
- if code is None:
30
- return 0
31
- if isinstance(code, int):
32
- return code
33
- print(f"✖️ 执行失败: {code}", file=sys.stderr)
34
- return 1
35
- except KeyboardInterrupt:
36
- print("✖️ 执行已取消", file=sys.stderr)
37
- return 130
38
- except Exception as e:
39
- print(f"✖️ 执行失败: {e}", file=sys.stderr)
40
- return 1
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: NOASSERTION
3
- """Shared SQLite JSON cache helpers for ok-cosmic scripts."""
4
-
5
- import json
6
- import os
7
- import re
8
- import sqlite3
9
- import sys
10
- import time
11
- from typing import Any, Dict, Iterable, Optional, Sequence
12
-
13
-
14
- IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
15
-
16
-
17
- def resolve_graph_db_path(config: Dict[str, Any], missing_message: str) -> str:
18
- """Resolve graph.dbPath relative to ok-cosmic.json's directory."""
19
- graph_config = config.get("graph", {})
20
- if not isinstance(graph_config, dict):
21
- graph_config = {}
22
-
23
- db_path = str(graph_config.get("dbPath", "")).strip()
24
- if not db_path:
25
- raise ValueError(missing_message)
26
-
27
- raw_db_path = os.path.expanduser(db_path)
28
- if os.path.isabs(raw_db_path):
29
- return os.path.normpath(raw_db_path)
30
-
31
- base_dir = str(config.get("__config_dir__", "")).strip() or os.getcwd()
32
- return os.path.normpath(os.path.abspath(os.path.join(base_dir, raw_db_path)))
33
-
34
-
35
- def _validate_identifier(value: str, label: str) -> str:
36
- if not IDENTIFIER_RE.match(value):
37
- raise ValueError(f"{label} 不是安全的 SQLite 标识符: {value}")
38
- return value
39
-
40
-
41
- class JsonSqliteCache:
42
- """Small SQLite cache storing JSON payload + updated_at with TTL."""
43
-
44
- def __init__(
45
- self,
46
- db_path: str,
47
- *,
48
- table_name: str,
49
- key_column: str,
50
- create_sql: str,
51
- ttl: int = 600,
52
- check_same_thread: bool = True,
53
- init_error_message: str = "初始化缓存表失败",
54
- write_error_message: str = "写入缓存失败",
55
- ):
56
- self.db_path = db_path
57
- self.table_name = _validate_identifier(table_name, "table_name")
58
- self.key_column = _validate_identifier(key_column, "key_column")
59
- self.ttl = ttl
60
- self.init_error_message = init_error_message
61
- self.write_error_message = write_error_message
62
- self._conn = sqlite3.connect(self.db_path, check_same_thread=check_same_thread)
63
- self._conn.execute("PRAGMA journal_mode=WAL")
64
- self._conn.execute("PRAGMA busy_timeout=5000")
65
- self._conn.row_factory = sqlite3.Row
66
- self._init_db(create_sql)
67
-
68
- def _init_db(self, create_sql: str) -> None:
69
- try:
70
- self._conn.execute(create_sql)
71
- self._conn.commit()
72
- except Exception as e:
73
- print(f" (DEBUG) {self.init_error_message}: {e}", file=sys.stderr)
74
-
75
- def get(self, key: str) -> Optional[Dict[str, Any]]:
76
- try:
77
- row = self._conn.execute(
78
- f"SELECT payload, updated_at FROM {self.table_name} WHERE {self.key_column} = ?",
79
- (key,),
80
- ).fetchone()
81
- if not row or (time.time() - row["updated_at"] > self.ttl):
82
- return None
83
- return json.loads(row["payload"])
84
- except Exception:
85
- return None
86
-
87
- def set_payload(
88
- self,
89
- key: str,
90
- payload: Dict[str, Any],
91
- *,
92
- extra_columns: Optional[Sequence[str]] = None,
93
- extra_values: Optional[Sequence[Any]] = None,
94
- ) -> None:
95
- extra_columns = tuple(extra_columns or ())
96
- extra_values = tuple(extra_values or ())
97
- if len(extra_columns) != len(extra_values):
98
- raise ValueError("extra_columns 和 extra_values 数量不一致")
99
- for column in extra_columns:
100
- _validate_identifier(column, "extra_column")
101
-
102
- columns = (self.key_column, *extra_columns, "payload", "updated_at")
103
- placeholders = ", ".join("?" for _ in columns)
104
- column_sql = ", ".join(columns)
105
- values = (
106
- key,
107
- *extra_values,
108
- json.dumps(payload, ensure_ascii=False),
109
- int(time.time()),
110
- )
111
-
112
- try:
113
- self._conn.execute(
114
- f"""
115
- INSERT OR REPLACE INTO {self.table_name}
116
- ({column_sql})
117
- VALUES ({placeholders})
118
- """,
119
- values,
120
- )
121
- self._conn.commit()
122
- except Exception as e:
123
- print(f" (DEBUG) {self.write_error_message}: {e}", file=sys.stderr)
124
-
125
- def remove(self, key: str) -> None:
126
- try:
127
- self._conn.execute(
128
- f"DELETE FROM {self.table_name} WHERE {self.key_column} = ?",
129
- (key,),
130
- )
131
- self._conn.commit()
132
- except Exception:
133
- pass
134
-
135
- def close(self) -> None:
136
- try:
137
- self._conn.close()
138
- except Exception:
139
- pass
140
-
141
- def __del__(self):
142
- self.close()
@@ -1,18 +0,0 @@
1
- #!/bin/bash
2
- # ok-cosmic-knowledge 离线 API 知识图谱构建工具
3
- # 用法: ./setup-mac.sh [参数]
4
-
5
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
- JAR_FILE="$SCRIPT_DIR/setup.jar"
7
-
8
- if ! command -v java &> /dev/null; then
9
- echo "错误: 未找到 java 命令,请安装 JDK 8+"
10
- exit 1
11
- fi
12
-
13
- if [ ! -f "$JAR_FILE" ]; then
14
- echo "请确认发布包完整,或重新获取包含 setup.jar 的安装包"
15
- exit 1
16
- fi
17
-
18
- java -jar "$JAR_FILE" "$@"
@@ -1,53 +0,0 @@
1
- @echo off
2
- setlocal
3
- REM ok-cosmic-knowledge 离线 API 知识图谱构建工具
4
- REM 用法: setup-windows.bat [参数]
5
-
6
- chcp 65001 >nul 2>nul
7
-
8
- set "SCRIPT_DIR=%~dp0"
9
- set "JAR_FILE=%SCRIPT_DIR%setup.jar"
10
- set "EXIT_CODE=0"
11
- set "PAUSE_ON_EXIT="
12
-
13
- REM Explorer 双击通常通过 cmd.exe /c 启动,执行结束后窗口会立即关闭。
14
- echo %CMDCMDLINE% | findstr /I /C:" /c " >nul && set "PAUSE_ON_EXIT=1"
15
-
16
- if not exist "%JAR_FILE%" (
17
- echo 错误: setup.jar 文件不存在: "%JAR_FILE%"
18
- echo 请确认发布包完整,或重新获取包含 setup.jar 的安装包
19
- set "EXIT_CODE=1"
20
- goto end
21
- )
22
-
23
- where java >nul 2>nul
24
- if errorlevel 1 (
25
- echo 错误: 未找到 java 命令,请先安装 JRE/JDK 并配置 PATH。
26
- set "EXIT_CODE=1"
27
- goto end
28
- )
29
-
30
- pushd "%SCRIPT_DIR%" >nul 2>nul
31
- if errorlevel 1 (
32
- echo 错误: 无法进入脚本目录: "%SCRIPT_DIR%"
33
- set "EXIT_CODE=1"
34
- goto end
35
- )
36
-
37
- java -jar "%JAR_FILE%" %*
38
- set "EXIT_CODE=%ERRORLEVEL%"
39
- popd >nul
40
-
41
- :end
42
- if defined PAUSE_ON_EXIT (
43
- echo.
44
- if "%EXIT_CODE%"=="0" (
45
- echo 执行完成,按任意键关闭窗口...
46
- ) else (
47
- echo 执行失败,退出码: %EXIT_CODE%
48
- echo 按任意键关闭窗口...
49
- )
50
- pause >nul
51
- )
52
-
53
- exit /b %EXIT_CODE%
@@ -1,363 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Static checks for ok-ksql generated PostgreSQL data-fix SQL files.
3
-
4
- The linter is intentionally dependency-free and conservative: it catches the
5
- high-risk patterns required by the ok-ksql skill, but it is not a full SQL
6
- parser. Fix reported ERROR items before delivery. WARN items document style
7
- preferences that may be acceptable only with an explicit reason.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import re
14
- import sys
15
- from dataclasses import dataclass
16
- from pathlib import Path
17
- from typing import Iterable, Iterator, Sequence
18
-
19
-
20
- TIMESTAMP_RE = re.compile(r"\b\d{12}\b")
21
- BACKUP_TABLE_RE = re.compile(r"\bbak_[a-zA-Z0-9_]+_(\d{12})\b", re.IGNORECASE)
22
- FILE_TS_RE = re.compile(r"ksql_[^/\\]*_(\d{12})\.txt$", re.IGNORECASE)
23
- HEADER_TS_RE = re.compile(r"备份表时间戳[::]\s*(\d{12})")
24
-
25
-
26
- @dataclass(frozen=True)
27
- class Finding:
28
- severity: str
29
- line: int
30
- message: str
31
-
32
- def format(self, path: Path) -> str:
33
- return f"{path}:{self.line}: {self.severity}: {self.message}"
34
-
35
-
36
- @dataclass(frozen=True)
37
- class Statement:
38
- text: str
39
- line: int
40
-
41
-
42
- def strip_comments_and_literals(sql: str) -> str:
43
- """Return SQL with comments and quoted literals replaced by spaces.
44
-
45
- This keeps line numbers stable while avoiding false positives from words
46
- appearing inside comments or strings.
47
- """
48
-
49
- out: list[str] = []
50
- i = 0
51
- n = len(sql)
52
- state = "normal"
53
- dollar_tag = ""
54
-
55
- while i < n:
56
- ch = sql[i]
57
- nxt = sql[i + 1] if i + 1 < n else ""
58
-
59
- if state == "normal":
60
- if ch == "-" and nxt == "-":
61
- out.extend([" ", " "])
62
- i += 2
63
- state = "line_comment"
64
- continue
65
- if ch == "/" and nxt == "*":
66
- out.extend([" ", " "])
67
- i += 2
68
- state = "block_comment"
69
- continue
70
- if ch == "'":
71
- out.append(" ")
72
- i += 1
73
- state = "single_quote"
74
- continue
75
- if ch == '"':
76
- out.append(" ")
77
- i += 1
78
- state = "double_quote"
79
- continue
80
- if ch == "$":
81
- match = re.match(r"\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$", sql[i:])
82
- if match:
83
- dollar_tag = match.group(0)
84
- out.extend(" " * len(dollar_tag))
85
- i += len(dollar_tag)
86
- state = "dollar_quote"
87
- continue
88
- out.append(ch)
89
- i += 1
90
- continue
91
-
92
- if state == "line_comment":
93
- out.append("\n" if ch == "\n" else " ")
94
- i += 1
95
- if ch == "\n":
96
- state = "normal"
97
- continue
98
-
99
- if state == "block_comment":
100
- if ch == "*" and nxt == "/":
101
- out.extend([" ", " "])
102
- i += 2
103
- state = "normal"
104
- else:
105
- out.append("\n" if ch == "\n" else " ")
106
- i += 1
107
- continue
108
-
109
- if state == "single_quote":
110
- if ch == "'" and nxt == "'":
111
- out.extend([" ", " "])
112
- i += 2
113
- else:
114
- out.append("\n" if ch == "\n" else " ")
115
- i += 1
116
- if ch == "'":
117
- state = "normal"
118
- continue
119
-
120
- if state == "double_quote":
121
- if ch == '"' and nxt == '"':
122
- out.extend([" ", " "])
123
- i += 2
124
- else:
125
- out.append("\n" if ch == "\n" else " ")
126
- i += 1
127
- if ch == '"':
128
- state = "normal"
129
- continue
130
-
131
- if state == "dollar_quote":
132
- if sql.startswith(dollar_tag, i):
133
- out.extend(" " * len(dollar_tag))
134
- i += len(dollar_tag)
135
- state = "normal"
136
- dollar_tag = ""
137
- else:
138
- out.append("\n" if ch == "\n" else " ")
139
- i += 1
140
- continue
141
-
142
- return "".join(out)
143
-
144
-
145
- def iter_statements(masked_sql: str) -> Iterator[Statement]:
146
- start = 0
147
- start_line = 1
148
- line = 1
149
-
150
- for idx, ch in enumerate(masked_sql):
151
- if ch == ";":
152
- text = masked_sql[start : idx + 1]
153
- if text.strip():
154
- yield Statement(text=text, line=start_line)
155
- start = idx + 1
156
- start_line = line
157
- if ch == "\n":
158
- line += 1
159
- if not masked_sql[start:idx].strip():
160
- start_line = line
161
-
162
- tail = masked_sql[start:]
163
- if tail.strip():
164
- yield Statement(text=tail, line=start_line)
165
-
166
-
167
- def line_of_offset(text: str, base_line: int, offset: int) -> int:
168
- return base_line + text[:offset].count("\n")
169
-
170
-
171
- def has_token(statement: str, token: str) -> bool:
172
- return re.search(rf"\b{re.escape(token)}\b", statement, re.IGNORECASE) is not None
173
-
174
-
175
- def lint_statement(stmt: Statement) -> list[Finding]:
176
- findings: list[Finding] = []
177
- compact = " ".join(stmt.text.split())
178
- if not compact:
179
- return findings
180
-
181
- first = re.match(r"^\s*(\w+)", compact)
182
- first_word = first.group(1).upper() if first else ""
183
-
184
- if first_word in {"UPDATE", "DELETE"} and not has_token(stmt.text, "WHERE"):
185
- findings.append(
186
- Finding(
187
- "ERROR",
188
- stmt.line,
189
- f"{first_word} 语句缺少 WHERE,禁止生成无范围更新/删除。",
190
- )
191
- )
192
-
193
- select_star_matches = list(re.finditer(r"\bSELECT\s+\*", stmt.text, re.IGNORECASE))
194
- if select_star_matches:
195
- is_backup = re.search(
196
- r"\bSELECT\s+\*\s+INTO\s+bak_[a-zA-Z0-9_]+_\d{12}\s+FROM\b",
197
- stmt.text,
198
- re.IGNORECASE,
199
- )
200
- if is_backup:
201
- if has_token(stmt.text, "WHERE"):
202
- findings.append(
203
- Finding(
204
- "ERROR",
205
- stmt.line,
206
- "备份语句必须整表备份,SELECT * INTO bak_... 不允许带 WHERE。",
207
- )
208
- )
209
- else:
210
- for match in select_star_matches:
211
- findings.append(
212
- Finding(
213
- "ERROR",
214
- line_of_offset(stmt.text, stmt.line, match.start()),
215
- "查询/验证语句禁止 SELECT *;只有整表备份 SELECT * INTO bak_... 例外。",
216
- )
217
- )
218
-
219
- if re.search(r"\bSELECT\s+\*\s+INTO\b", stmt.text, re.IGNORECASE) and not re.search(
220
- r"\bSELECT\s+\*\s+INTO\s+bak_[a-zA-Z0-9_]+_\d{12}\s+FROM\b",
221
- stmt.text,
222
- re.IGNORECASE,
223
- ):
224
- findings.append(
225
- Finding(
226
- "ERROR",
227
- stmt.line,
228
- "备份表名必须形如 bak_<原表或业务缩写>_<yyyyMMddHHmm>。",
229
- )
230
- )
231
-
232
- for match in re.finditer(r"\bEXISTS\b", stmt.text, re.IGNORECASE):
233
- findings.append(
234
- Finding(
235
- "WARN",
236
- line_of_offset(stmt.text, stmt.line, match.start()),
237
- "SQL 可读性偏好:成员关系/半连接默认使用 IN,只有 IN 改变语义时才保留 EXISTS 并说明原因。",
238
- )
239
- )
240
-
241
- if re.search(r"\bUPDATE\b.+\bJOIN\b", compact, re.IGNORECASE):
242
- findings.append(
243
- Finding(
244
- "WARN",
245
- stmt.line,
246
- "PostgreSQL 多表更新优先使用 UPDATE ... FROM ... WHERE ...,不要使用 MySQL 风格 UPDATE ... JOIN。",
247
- )
248
- )
249
-
250
- if re.search(r"=\s*NULL\b|\bNULL\s*=", stmt.text, re.IGNORECASE):
251
- findings.append(
252
- Finding(
253
- "ERROR",
254
- stmt.line,
255
- "NULL 判断必须使用 IS NULL / IS NOT NULL,不能使用 = NULL。",
256
- )
257
- )
258
-
259
- if re.search(r"<>|!=", stmt.text) and has_token(stmt.text, "NULL"):
260
- findings.append(
261
- Finding(
262
- "WARN",
263
- stmt.line,
264
- "涉及 NULL 的不等比较需确认语义;PostgreSQL 可优先使用 IS DISTINCT FROM。",
265
- )
266
- )
267
-
268
- return findings
269
-
270
-
271
- def lint_timestamps(path: Path, raw_sql: str) -> list[Finding]:
272
- findings: list[Finding] = []
273
- backup_timestamps = set(BACKUP_TABLE_RE.findall(raw_sql))
274
-
275
- if len(backup_timestamps) > 1:
276
- findings.append(
277
- Finding(
278
- "ERROR",
279
- 1,
280
- "同一 SQL 文件中出现多个备份表时间戳;桌面文件、备份表和文件头时间戳必须一致。",
281
- )
282
- )
283
-
284
- file_match = FILE_TS_RE.search(path.name)
285
- if file_match and backup_timestamps and file_match.group(1) not in backup_timestamps:
286
- findings.append(
287
- Finding(
288
- "ERROR",
289
- 1,
290
- f"文件名时间戳 {file_match.group(1)} 与备份表时间戳 {', '.join(sorted(backup_timestamps))} 不一致。",
291
- )
292
- )
293
-
294
- header_matches = list(HEADER_TS_RE.finditer(raw_sql))
295
- header_timestamps = {m.group(1) for m in header_matches}
296
- if len(header_timestamps) > 1:
297
- findings.append(Finding("ERROR", 1, "文件头出现多个不同的备份表时间戳。"))
298
- if header_timestamps and backup_timestamps and header_timestamps != backup_timestamps:
299
- findings.append(
300
- Finding(
301
- "ERROR",
302
- 1,
303
- f"文件头时间戳 {', '.join(sorted(header_timestamps))} 与备份表时间戳 {', '.join(sorted(backup_timestamps))} 不一致。",
304
- )
305
- )
306
-
307
- return findings
308
-
309
-
310
- def lint_file(path: Path) -> list[Finding]:
311
- raw_sql = path.read_text(encoding="utf-8")
312
- masked_sql = strip_comments_and_literals(raw_sql)
313
-
314
- findings: list[Finding] = []
315
- findings.extend(lint_timestamps(path, raw_sql))
316
- for stmt in iter_statements(masked_sql):
317
- findings.extend(lint_statement(stmt))
318
- return sorted(findings, key=lambda f: (f.line, f.severity, f.message))
319
-
320
-
321
- def parse_args(argv: Sequence[str]) -> argparse.Namespace:
322
- parser = argparse.ArgumentParser(
323
- description="Lint ok-ksql generated PostgreSQL data-fix SQL files."
324
- )
325
- parser.add_argument("paths", nargs="+", type=Path, help="SQL/.txt files to lint")
326
- parser.add_argument(
327
- "--strict",
328
- action="store_true",
329
- help="treat WARN findings as failures",
330
- )
331
- return parser.parse_args(argv)
332
-
333
-
334
- def main(argv: Sequence[str] | None = None) -> int:
335
- args = parse_args(sys.argv[1:] if argv is None else argv)
336
- all_findings: list[tuple[Path, Finding]] = []
337
-
338
- for path in args.paths:
339
- if not path.exists():
340
- print(f"{path}:1: ERROR: 文件不存在。", file=sys.stderr)
341
- return 2
342
- if path.is_dir():
343
- print(f"{path}:1: ERROR: 请输入 SQL/.txt 文件,不能是目录。", file=sys.stderr)
344
- return 2
345
- for finding in lint_file(path):
346
- all_findings.append((path, finding))
347
-
348
- for path, finding in all_findings:
349
- print(finding.format(path))
350
-
351
- error_count = sum(1 for _, f in all_findings if f.severity == "ERROR")
352
- warn_count = sum(1 for _, f in all_findings if f.severity == "WARN")
353
- print(f"SUMMARY: {error_count} error(s), {warn_count} warning(s)")
354
-
355
- if error_count:
356
- return 1
357
- if args.strict and warn_count:
358
- return 1
359
- return 0
360
-
361
-
362
- if __name__ == "__main__":
363
- raise SystemExit(main())