@antprofuse/saddle-islands-tools 0.1.3 → 0.1.4
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.
package/package.json
CHANGED
package/tools/cli/main.py
CHANGED
|
@@ -10,6 +10,9 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def main():
|
|
13
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'iteration':
|
|
14
|
+
from tools.modules.workflow.iteration import main as iteration_main
|
|
15
|
+
return iteration_main(sys.argv[2:])
|
|
13
16
|
if len(sys.argv) > 1 and sys.argv[1] == 'identity':
|
|
14
17
|
from tools.core.semantic_identity import main as identity_main
|
|
15
18
|
sys.argv = [sys.argv[0] + ' identity', *sys.argv[2:]]
|
|
@@ -27,7 +30,7 @@ def main():
|
|
|
27
30
|
entry = 'minifish.mjs' if command in ('build', 'package', 'plugin') else 'minifish.py'
|
|
28
31
|
return subprocess.call(['node' if entry.endswith('.mjs') else sys.executable, str(Path(__file__).with_name(entry)), *args])
|
|
29
32
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
30
|
-
parser.add_argument('command', choices=['identity', 'backend', 'minifish'], help='semantic identities, backend tests or Minifish tests')
|
|
33
|
+
parser.add_argument('command', choices=['identity', 'backend', 'minifish', 'iteration'], help='semantic identities, backend tests or Minifish tests')
|
|
31
34
|
parser.parse_args()
|
|
32
35
|
|
|
33
36
|
|
package/tools/docs/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| 工作 | 命令入口 | 说明 |
|
|
8
8
|
|---|---|---|
|
|
9
|
+
| 迭代节点渲染与Git目录门禁 | `python3 TOOL_ROOT/tools/cli/main.py iteration` | [迭代参数与目录门禁](iteration.md) |
|
|
9
10
|
| 语义身份生成、登记、检查 | `python3 TOOL_ROOT/tools/cli/main.py identity` | [语义身份](semantic-identity.md) |
|
|
10
11
|
| 后端场景列举与执行 | `python3 TOOL_ROOT/tools/cli/main.py backend` | [后端接入](backend-testing.md) |
|
|
11
12
|
| Minifish编译、制品、运行、报告 | `python3 TOOL_ROOT/tools/cli/main.py minifish` | [Minifish接入](minifish-testing.md) |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 迭代节点渲染
|
|
2
|
+
|
|
3
|
+
通过独立工具集消费冻结的workflow Skill参数schema和阶段元数据,不直接调用CoTask,也不启动任务。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 tools/cli/main.py iteration render \
|
|
7
|
+
--workflow /absolute/path/to/workflow-skill \
|
|
8
|
+
--input /absolute/path/to/iteration.json \
|
|
9
|
+
--output /absolute/path/to/new-review.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
输入结构见workflow包的 `resources/iteration.schema.json`,固定阶段规则见 `resources/stage-meta.json`。发起者冻结Skill版本后填写实例;阶段实例不允许覆盖全局git或bizdir。`sharedOutput` 相对全局共享根,正式输出目录与单文件权限由阶段元数据派生,非用户手填。
|
|
13
|
+
|
|
14
|
+
输出JSON包含公共上下文和各执行节点的Markdown正文,以及供目录门禁消费的结构化路径权限。调用方使用 `context` 原文更新节点,不能把整个JSON塞进节点说明。不会覆盖已有输出文件。未知参数、缺失必填值、共享根不存在、重复ID/工作分支、共享路径重叠或逃逸、未解析占位符都以非零退出拒绝。
|
|
15
|
+
|
|
16
|
+
目前是启动前审阅渲染:`acceptedInput` 可指向等待上游检查点生成的登记文件;`inputCommit` 在检查点接受成果后绑定。渲染成功不表示后续分支、输入文件或运行环境已准备完毕,也不授予执行许可。输出还包含阶段检查点及最终检查点的Markdown;工具不自动构建依赖图或调用平台。
|
|
17
|
+
|
|
18
|
+
本工具对随Skill schema使用的受限JSON Schema关键字执行校验,不冒充通用JSON Schema实现;未知关键字拒绝,不静默忽略。Git差异目录门禁见下。
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Git目录门禁
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 tools/cli/main.py iteration diff-review \
|
|
25
|
+
--workflow /absolute/path/to/workflow-skill \
|
|
26
|
+
--input /absolute/path/to/iteration.json \
|
|
27
|
+
--repository /absolute/path/to/business-checkout \
|
|
28
|
+
--base <full-base-commit> --head <full-candidate-commit> \
|
|
29
|
+
--stage-id D --output /absolute/path/to/new-diff-review.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
基线须为候选的祖先,两个端点必须是完整提交ID。范围来自同一实例参数和冻结阶段元数据,不接受手填任意白名单。集成交付可重复传 `--stage-id` 检查已接受阶段范围的并集。退出0表示路径检查通过,3表示有越界路径(仍写出审阅报告),2表示参数或读取错误。只读Git,不暂存、不修改、不提交。
|
|
33
|
+
|
|
34
|
+
报告按目标父目录汇总文件变更类型和增删行,列所有变更文件;重命名同时检查源、目标路径,删除不能逃避检查。本门禁只核Git记录的变更路径,不追踪链接目标或增加文件类型门禁。该门禁不判断业务内容是否正确,不扫描未提交内容,也不代替缓存、依赖、报告等产物类型审查。
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Read-only commit diff path gate and directory review report."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
import html
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from .iteration import Invalid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def git(repo, *args):
|
|
11
|
+
p = subprocess.run(['git', '-C', str(repo), *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
12
|
+
if p.returncode:
|
|
13
|
+
raise Invalid(p.stderr.decode('utf-8', 'replace').strip())
|
|
14
|
+
return p.stdout
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def review(repo, base, head, nodes):
|
|
18
|
+
for commit in (base, head):
|
|
19
|
+
if not re.fullmatch('[0-9a-f]{40}', commit):
|
|
20
|
+
raise Invalid('Diff endpoints must be full immutable commit IDs')
|
|
21
|
+
git(repo, 'cat-file', '-e', commit + '^{commit}')
|
|
22
|
+
git(repo, 'merge-base', '--is-ancestor', base, head)
|
|
23
|
+
directories = [p for n in nodes for p in n['outputDirectories']]
|
|
24
|
+
files = {p for n in nodes for p in n['outputFiles']}
|
|
25
|
+
def allowed(path):
|
|
26
|
+
return path in files or any(path.startswith(d + '/') for d in directories)
|
|
27
|
+
changed = git(repo, 'diff', '--no-ext-diff', '--no-textconv', '--name-status', '-z', '--find-renames', base, head, '--').decode().split('\0')
|
|
28
|
+
entries = []
|
|
29
|
+
while changed and changed[0]:
|
|
30
|
+
status = changed.pop(0)
|
|
31
|
+
old = changed.pop(0)
|
|
32
|
+
new = changed.pop(0) if status.startswith(('R', 'C')) else old
|
|
33
|
+
entries.append({'status':status, 'old':old, 'new':new})
|
|
34
|
+
stats = {}
|
|
35
|
+
for item in git(repo, 'diff', '--no-ext-diff', '--no-textconv', '--numstat', '-z', '--no-renames', base, head, '--').decode().split('\0'):
|
|
36
|
+
if item:
|
|
37
|
+
add, delete, path = item.split('\t', 2)
|
|
38
|
+
stats[path] = (int(add) if add != '-' else 0, int(delete) if delete != '-' else 0, add == '-')
|
|
39
|
+
violations = []
|
|
40
|
+
groups = defaultdict(lambda: dict(A=0, M=0, D=0, R=0, T=0, added=0, deleted=0, binary=0))
|
|
41
|
+
for entry in entries:
|
|
42
|
+
paths = {entry['old'], entry['new']}
|
|
43
|
+
why = []
|
|
44
|
+
for path in sorted(paths):
|
|
45
|
+
if not allowed(path):
|
|
46
|
+
why.append('越界路径:' + path)
|
|
47
|
+
entry['issues'] = why
|
|
48
|
+
violations.extend(why)
|
|
49
|
+
group = groups[str(PurePosixPath(entry['new']).parent)]
|
|
50
|
+
category = entry['status'][0]
|
|
51
|
+
if category not in ('A', 'M', 'D', 'R', 'T'):
|
|
52
|
+
why.append('未支持的变更类型:' + entry['status'])
|
|
53
|
+
violations.append(why[-1])
|
|
54
|
+
else:
|
|
55
|
+
group[category] += 1
|
|
56
|
+
for path in paths:
|
|
57
|
+
add, delete, binary = stats.get(path, (0, 0, False))
|
|
58
|
+
group['added'] += add; group['deleted'] += delete; group['binary'] += int(binary)
|
|
59
|
+
def escape(value):
|
|
60
|
+
return html.escape(value).replace('|', '|').replace('`', '`').replace('\n', ' ').replace('\r', ' ')
|
|
61
|
+
lines = ['# Git目录差异审阅', '', '- 基线提交:`' + base + '`', '- 候选提交:`' + head + '`',
|
|
62
|
+
'- 路径门禁:' + ('未通过' if violations else '通过'), '',
|
|
63
|
+
'路径门禁仅检查已提交的变更路径范围,不替代内容、业务正确性或未提交文件检查。重命名按目标目录计数,旧、新路径均核权限;增删行使用未合并重命名的Git统计,二进制不虚构行数。', '',
|
|
64
|
+
'| 目录 | 新增 | 修改 | 删除 | 重命名 | 类型变化 | 增行 | 删行 | 二进制项 |',
|
|
65
|
+
'|---|---:|---:|---:|---:|---:|---:|---:|---:|']
|
|
66
|
+
for path, g in sorted(groups.items()):
|
|
67
|
+
lines.append('| ' + escape(path) + ' | ' + ' | '.join(str(g[k]) for k in ('A','M','D','R','T','added','deleted','binary')) + ' |')
|
|
68
|
+
if not entries:
|
|
69
|
+
lines += ['', '无已提交变更。']
|
|
70
|
+
lines += ['', '## 文件明细', '']
|
|
71
|
+
for e in entries:
|
|
72
|
+
path = e['old'] + ' → ' + e['new'] if e['old'] != e['new'] else e['new']
|
|
73
|
+
lines.append('- ' + e['status'] + ':' + escape(path) + (';' + escape(';'.join(e['issues'])) if e['issues'] else ''))
|
|
74
|
+
return {'passed':not violations, 'entries':entries, 'violations':violations, 'markdown':'\n'.join(lines)+'\n'}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Validate explicit iteration parameters and render reviewable stage descriptions.
|
|
2
|
+
|
|
3
|
+
Schema and stage policy are supplied by a frozen workflow Skill, not duplicated here.
|
|
4
|
+
Only the documented schema vocabulary is supported; unknown validation keywords fail.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Invalid(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate(value, schema, at='$'):
|
|
18
|
+
supported = {'$schema', 'title', 'description', 'type', 'required', 'properties',
|
|
19
|
+
'additionalProperties', 'items', 'minItems', 'minLength', 'pattern', 'enum', 'const'}
|
|
20
|
+
if set(schema) - supported:
|
|
21
|
+
raise Invalid(f'{at}: unsupported schema keywords {sorted(set(schema) - supported)}')
|
|
22
|
+
kinds = {'object': dict, 'array': list, 'string': str}
|
|
23
|
+
if 'type' in schema and (schema['type'] not in kinds or type(value) is not kinds[schema['type']]):
|
|
24
|
+
raise Invalid(f'{at}: expected {schema["type"]}')
|
|
25
|
+
if 'const' in schema and (type(value) is not type(schema['const']) or value != schema['const']):
|
|
26
|
+
raise Invalid(f'{at}: invalid schema version/value')
|
|
27
|
+
if 'enum' in schema and value not in schema['enum']:
|
|
28
|
+
raise Invalid(f'{at}: unsupported value')
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
props = schema.get('properties', {})
|
|
31
|
+
missing = set(schema.get('required', [])) - value.keys()
|
|
32
|
+
extra = value.keys() - props.keys()
|
|
33
|
+
if missing or (extra and schema.get('additionalProperties') is False):
|
|
34
|
+
raise Invalid(f'{at}: missing={sorted(missing)}, unknown={sorted(extra)}')
|
|
35
|
+
for key in props.keys() & value.keys():
|
|
36
|
+
validate(value[key], props[key], at + '.' + key)
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
if len(value) < schema.get('minItems', 0):
|
|
39
|
+
raise Invalid(f'{at}: insufficient items')
|
|
40
|
+
for i, item in enumerate(value):
|
|
41
|
+
validate(item, schema.get('items', {}), f'{at}[{i}]')
|
|
42
|
+
if isinstance(value, str):
|
|
43
|
+
if len(value) < schema.get('minLength', 0) or ('pattern' in schema and not re.search(schema['pattern'], value)):
|
|
44
|
+
raise Invalid(f'{at}: malformed value')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def relative(value):
|
|
48
|
+
p = PurePosixPath(value)
|
|
49
|
+
if p.is_absolute() or any(x in ('', '.', '..') for x in value.split('/')) or '\\' in value:
|
|
50
|
+
raise Invalid('Not a normalized repository-relative path: ' + value)
|
|
51
|
+
return p
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def branch(value):
|
|
55
|
+
if (value.startswith(('-', '/')) or value.endswith(('/', '.')) or '..' in value or '@{' in value
|
|
56
|
+
or re.search(r'[\s~^:?*\[\\]', value) or any(x in ('', '.') or x.startswith('.') or x.endswith('.lock') for x in value.split('/')) or value == '@'):
|
|
57
|
+
raise Invalid('Invalid branch name: ' + value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render(config, workflow):
|
|
61
|
+
workflow = Path(workflow)
|
|
62
|
+
schema = json.loads((workflow / 'resources/iteration.schema.json').read_text())
|
|
63
|
+
meta = json.loads((workflow / 'resources/stage-meta.json').read_text())
|
|
64
|
+
validate(config, schema)
|
|
65
|
+
if meta['version'] != config['metaVersion']:
|
|
66
|
+
raise Invalid('Stage metadata version mismatch')
|
|
67
|
+
relative(config['bizdir'])
|
|
68
|
+
branch(config['git']['branch'])
|
|
69
|
+
# Operational input paths are explicit absolute paths, not cwd-relative guesses.
|
|
70
|
+
for value in [config['persistentDeliveryDirectory'], config['sharedRoot'], config['source']['root'], config['skillsRoot'], config['toolsRoot'], config['frameworkSkill'], *config['generated'].values()]:
|
|
71
|
+
if not Path(value).is_absolute():
|
|
72
|
+
raise Invalid('Expected absolute operational input path: ' + value)
|
|
73
|
+
shared = Path(config['sharedRoot']).resolve(strict=True)
|
|
74
|
+
if not shared.is_dir():
|
|
75
|
+
raise Invalid('Shared root must exist as a directory')
|
|
76
|
+
def field(label, value):
|
|
77
|
+
return f'- {label}:`{value}`'
|
|
78
|
+
def expand(template, params):
|
|
79
|
+
def get(match):
|
|
80
|
+
value = params
|
|
81
|
+
for key in match.group(1).split('.'):
|
|
82
|
+
if not isinstance(value, dict) or key not in value:
|
|
83
|
+
raise Invalid('Unknown template parameter: ' + match.group(1))
|
|
84
|
+
value = value[key]
|
|
85
|
+
if not isinstance(value, str):
|
|
86
|
+
raise Invalid('Template value must be a string')
|
|
87
|
+
return value
|
|
88
|
+
result = re.sub(r'\{([A-Za-z][A-Za-z0-9_.]*)\}', get, template)
|
|
89
|
+
if '{' in result or '}' in result:
|
|
90
|
+
raise Invalid('Unresolved template parameter')
|
|
91
|
+
return result
|
|
92
|
+
nodes, ids, branches, outputs = [], set(), set(), []
|
|
93
|
+
for stage in config['stages']:
|
|
94
|
+
if stage['id'] in ids or stage['workBranch'] in branches:
|
|
95
|
+
raise Invalid('Duplicate stage ID or work branch')
|
|
96
|
+
ids.add(stage['id']); branches.add(stage['workBranch']); branch(stage['workBranch'])
|
|
97
|
+
relative(stage['sharedOutput'])
|
|
98
|
+
output = (shared / stage['sharedOutput']).resolve()
|
|
99
|
+
if not output.is_relative_to(shared) or output == shared:
|
|
100
|
+
raise Invalid('Shared output escapes its root')
|
|
101
|
+
if any(output == old or output.is_relative_to(old) or old.is_relative_to(output) for old in outputs):
|
|
102
|
+
raise Invalid('Overlapping shared output directories')
|
|
103
|
+
outputs.append(output)
|
|
104
|
+
if not Path(stage['acceptedInput']).is_absolute():
|
|
105
|
+
raise Invalid('Accepted input registry must be an absolute path')
|
|
106
|
+
m = meta['stages'][stage['stage']]
|
|
107
|
+
params = {**config, **stage}
|
|
108
|
+
directories = [expand(v, params) for _, v in m['outputDirectories']]
|
|
109
|
+
files = [expand(v, params) for _, v in m['outputFiles']]
|
|
110
|
+
for p in directories + files:
|
|
111
|
+
if not relative(p).is_relative_to(relative(config['bizdir'])) or p == config['bizdir']:
|
|
112
|
+
raise Invalid('Output outside business directory')
|
|
113
|
+
lines = ['### 输入', '', field('工作分支', stage['workBranch'])] + [field(k, 'localPath:' + expand(v, params)) for k, v in m['inputs']]
|
|
114
|
+
if 'inputCommit' in stage:
|
|
115
|
+
lines.append(field('阶段输入提交', stage['inputCommit']))
|
|
116
|
+
if 'group' in stage:
|
|
117
|
+
lines.append(field('业务分组', stage['group']))
|
|
118
|
+
lines += ['', '### 执行', '', f'使用 `{m["skill"]}` 完成{m["label"]}阶段。', '',
|
|
119
|
+
field('Skill入口', config['skillsRoot'] + '/' + stage['stage'] + '/SKILL.md'), '', '### 交付', '']
|
|
120
|
+
lines += [field(k, 'gitBranchPath:' + expand(v, params)) for k, v in m['outputDirectories'] + m['outputFiles']]
|
|
121
|
+
lines.append(field('任务信息目录', 'localPath:' + str(output)))
|
|
122
|
+
lines += ['', meta['gitDeliveryInstruction'], '', field(meta['deliveryGate']['label'], meta['deliveryGate']['command']), '', meta['deliveryGate']['scope']]
|
|
123
|
+
nodes.append({'id':stage['id'], 'stage':stage['stage'], 'context':'\n'.join(lines),
|
|
124
|
+
'workBranch':stage['workBranch'], 'outputDirectories':directories,
|
|
125
|
+
'outputFiles':files, 'sharedOutput':str(output)})
|
|
126
|
+
common = '\n'.join(['### 全局参数', ''] + [field(k, v) for k, v in [
|
|
127
|
+
('Git仓库',config['git']['url']), ('基线分支',config['git']['branch']), ('基线提交',config['git']['commit']),
|
|
128
|
+
('业务目录',config['bizdir']), ('业务源版本',config['source']['commit']), ('业务源根目录',config['source']['root']),
|
|
129
|
+
('共享根目录',str(shared)), ('Skill根目录',config['skillsRoot']), ('工具目录',config['toolsRoot'])]])
|
|
130
|
+
def checkpoint(template, params, extra_inputs=()):
|
|
131
|
+
return '\n'.join(['### 输入', ''] + [field(k, ('localPath:' if '目录' in k else '') + expand(v, params)) for k, v in template['inputs']] + list(extra_inputs) +
|
|
132
|
+
['', '### 审核', '', template['review'], '', '### 交付', ''] +
|
|
133
|
+
[field(k, 'localPath:' + expand(v, params)) for k, v in template['outputs']])
|
|
134
|
+
reserved = {n['id'] + '-review' for n in nodes} | {'delivery'}
|
|
135
|
+
if ids & reserved:
|
|
136
|
+
raise Invalid('Stage ID conflicts with a checkpoint ID')
|
|
137
|
+
checkpoints = [{'id':n['id']+'-review', 'context':checkpoint(meta['checkpoint'],
|
|
138
|
+
{**config, **n, 'stageOutput':n['sharedOutput']})} for n in nodes]
|
|
139
|
+
final = {'id':'delivery', 'context':checkpoint(meta['delivery'], config,
|
|
140
|
+
[field(n['id']+'工作分支', n['workBranch']) for n in nodes])}
|
|
141
|
+
return {'schemaVersion':1,'metaVersion':meta['version'],'commonContext':common,'nodes':nodes,
|
|
142
|
+
'checkpoints':checkpoints,'delivery':final}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv=None):
|
|
146
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
147
|
+
parser.add_argument('action', choices=['render', 'diff-review'])
|
|
148
|
+
parser.add_argument('--input', required=True)
|
|
149
|
+
parser.add_argument('--workflow', required=True, help='Frozen workflow Skill directory')
|
|
150
|
+
parser.add_argument('--output', required=True, help='Review JSON; node contexts are Markdown')
|
|
151
|
+
parser.add_argument('--repository')
|
|
152
|
+
parser.add_argument('--base')
|
|
153
|
+
parser.add_argument('--head')
|
|
154
|
+
parser.add_argument('--stage-id', action='append')
|
|
155
|
+
args = parser.parse_args(argv)
|
|
156
|
+
try:
|
|
157
|
+
result = render(json.loads(Path(args.input).read_text()), args.workflow)
|
|
158
|
+
target = Path(args.output)
|
|
159
|
+
if target.exists():
|
|
160
|
+
raise Invalid('Output already exists; choose a new review artifact')
|
|
161
|
+
if args.action == 'diff-review':
|
|
162
|
+
if not all((args.repository, args.base, args.head, args.stage_id)):
|
|
163
|
+
raise Invalid('diff-review requires repository, base, head and stage-id')
|
|
164
|
+
selected = [n for n in result['nodes'] if n['id'] in args.stage_id]
|
|
165
|
+
if {n['id'] for n in selected} != set(args.stage_id):
|
|
166
|
+
raise Invalid('Unknown stage ID in diff-review scope')
|
|
167
|
+
from .diff_review import review
|
|
168
|
+
checked = review(args.repository, args.base, args.head, selected)
|
|
169
|
+
target.write_text(checked['markdown'])
|
|
170
|
+
return 0 if checked['passed'] else 3
|
|
171
|
+
target.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
|
|
172
|
+
except (ValueError, KeyError, OSError) as error:
|
|
173
|
+
print(str(error), file=sys.stderr)
|
|
174
|
+
return 2
|
|
175
|
+
return 0
|