@antprofuse/saddle-islands-tools 0.0.0-stage → 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 +15 -4
- package/tools/adapters/islands/trigger_source.py +53 -0
- package/tools/cli/backend.mjs +24 -0
- package/tools/cli/main.py +38 -0
- package/tools/cli/minifish.mjs +38 -0
- package/tools/cli/minifish.py +52 -0
- package/tools/core/package-identity.mjs +23 -0
- package/tools/core/package_identity.py +58 -0
- package/tools/core/semantic_identity.py +149 -0
- package/tools/core/test_semantic_identity.py +136 -0
- package/tools/docs/README.md +43 -0
- package/tools/docs/backend-testing.md +36 -0
- package/tools/docs/iteration.md +34 -0
- package/tools/docs/minifish-testing.md +32 -0
- package/tools/docs/semantic-identity.md +61 -0
- package/tools/modules/backend/actions.mjs +10 -0
- package/tools/modules/backend/assert.mjs +20 -0
- package/tools/modules/backend/context-receiver.py +30 -0
- package/tools/modules/backend/facility.test.mjs +44 -0
- package/tools/modules/backend/grouped_context.py +100 -0
- package/tools/modules/backend/grpc.mjs +57 -0
- package/tools/modules/backend/index.mjs +9 -0
- package/tools/modules/backend/input-tree.mjs +14 -0
- package/tools/modules/backend/logs.mjs +24 -0
- package/tools/modules/backend/logs.test.mjs +28 -0
- package/tools/modules/backend/metadata.mjs +66 -0
- package/tools/modules/backend/package-lock.json +362 -0
- package/tools/modules/backend/package.json +15 -0
- package/tools/modules/backend/paths.mjs +11 -0
- package/tools/modules/backend/product.mjs +48 -0
- package/tools/modules/backend/rejection-logs.mjs +33 -0
- package/tools/modules/backend/report.mjs +13 -0
- package/tools/modules/backend/report.test.mjs +13 -0
- package/tools/modules/backend/run.mjs +50 -0
- package/tools/modules/backend/util.mjs +24 -0
- package/tools/modules/minifish/checks/guard.test.mjs +13 -0
- package/tools/modules/minifish/checks/package.test.mjs +23 -0
- package/tools/modules/minifish/compiler/build-case.mjs +39 -0
- package/tools/modules/minifish/compiler/case-plan.mjs +175 -0
- package/tools/modules/minifish/compiler/config.mjs +29 -0
- package/tools/modules/minifish/compiler/observation-module-name.mjs +2 -0
- package/tools/modules/minifish/compiler/package-runner.mjs +43 -0
- package/tools/modules/minifish/compiler/semantic-module-gate.mjs +27 -0
- package/tools/modules/minifish/compiler/tool-identity.mjs +14 -0
- package/tools/modules/minifish/harness/batch_cases.py +91 -0
- package/tools/modules/minifish/harness/batch_plugin_groups.py +56 -0
- package/tools/modules/minifish/harness/batch_report.py +104 -0
- package/tools/modules/minifish/harness/browser.mjs +146 -0
- package/tools/modules/minifish/harness/build-plugin-artifact.mjs +25 -0
- package/tools/modules/minifish/harness/capture_contract.py +78 -0
- package/tools/modules/minifish/harness/carousel-observation.mjs +62 -0
- package/tools/modules/minifish/harness/carriers.mjs +49 -0
- package/tools/modules/minifish/harness/check-independent-batch.py +84 -0
- package/tools/modules/minifish/harness/check-runner-isolation.py +102 -0
- package/tools/modules/minifish/harness/component_runtime.py +125 -0
- package/tools/modules/minifish/harness/contract-value.mjs +32 -0
- package/tools/modules/minifish/harness/deferred-visual.mjs +21 -0
- package/tools/modules/minifish/harness/device-prepare.mjs +26 -0
- package/tools/modules/minifish/harness/execution-evidence.mjs +37 -0
- package/tools/modules/minifish/harness/foreground.mjs +20 -0
- package/tools/modules/minifish/harness/header-layout.mjs +47 -0
- package/tools/modules/minifish/harness/held-snapshot.mjs +67 -0
- package/tools/modules/minifish/harness/host-device-fixture.mjs +28 -0
- package/tools/modules/minifish/harness/host-language.mjs +73 -0
- package/tools/modules/minifish/harness/host-native-ui.mjs +39 -0
- package/tools/modules/minifish/harness/host-navigation.mjs +96 -0
- package/tools/modules/minifish/harness/image-loading.mjs +35 -0
- package/tools/modules/minifish/harness/independent-case-runner.mjs +175 -0
- package/tools/modules/minifish/harness/independent-visual.mjs +107 -0
- package/tools/modules/minifish/harness/inline-svg.mjs +32 -0
- package/tools/modules/minifish/harness/input-lifecycle-evidence.mjs +16 -0
- package/tools/modules/minifish/harness/jsapi.mjs +92 -0
- package/tools/modules/minifish/harness/loading-input.mjs +52 -0
- package/tools/modules/minifish/harness/loading-observation.mjs +25 -0
- package/tools/modules/minifish/harness/local-navigation-evidence.mjs +82 -0
- package/tools/modules/minifish/harness/miniapp-screenshot.mjs +38 -0
- package/tools/modules/minifish/harness/native-dom-sampling.mjs +40 -0
- package/tools/modules/minifish/harness/native-enabled.mjs +11 -0
- package/tools/modules/minifish/harness/native-rpc-failure.mjs +11 -0
- package/tools/modules/minifish/harness/native-state.mjs +14 -0
- package/tools/modules/minifish/harness/native-ui-contract.mjs +29 -0
- package/tools/modules/minifish/harness/navigation.mjs +19 -0
- package/tools/modules/minifish/harness/network_isolation.py +134 -0
- package/tools/modules/minifish/harness/painted-source-image.mjs +54 -0
- package/tools/modules/minifish/harness/playwright.mjs +1 -0
- package/tools/modules/minifish/harness/plugin-events.mjs +16 -0
- package/tools/modules/minifish/harness/plugin-package.mjs +23 -0
- package/tools/modules/minifish/harness/plugin_artifact.py +23 -0
- package/tools/modules/minifish/harness/product_identity.py +8 -0
- package/tools/modules/minifish/harness/project_config.py +14 -0
- package/tools/modules/minifish/harness/prototype-visual.mjs +123 -0
- package/tools/modules/minifish/harness/region-order.mjs +16 -0
- package/tools/modules/minifish/harness/renderer-image-evidence.mjs +32 -0
- package/tools/modules/minifish/harness/request-guard.mjs +65 -0
- package/tools/modules/minifish/harness/request-plan.mjs +124 -0
- package/tools/modules/minifish/harness/response-consumption.mjs +106 -0
- package/tools/modules/minifish/harness/retained-page-lifecycle.mjs +48 -0
- package/tools/modules/minifish/harness/rpc-wire.mjs +34 -0
- package/tools/modules/minifish/harness/run-batch.py +133 -0
- package/tools/modules/minifish/harness/run-platform.py +293 -0
- package/tools/modules/minifish/harness/runtime_isolation.py +71 -0
- package/tools/modules/minifish/harness/scroll-container-observation.mjs +40 -0
- package/tools/modules/minifish/harness/semantic-anchor.mjs +139 -0
- package/tools/modules/minifish/harness/semantic-observation.mjs +197 -0
- package/tools/modules/minifish/harness/skeleton-appearance.mjs +49 -0
- package/tools/modules/minifish/harness/target-actions.mjs +120 -0
- package/tools/modules/minifish/harness/test_batch_concurrency.py +125 -0
- package/tools/modules/minifish/harness/test_batch_plugin_groups.py +97 -0
- package/tools/modules/minifish/harness/test_batch_runner.py +107 -0
- package/tools/modules/minifish/harness/test_common_report.py +47 -0
- package/tools/modules/minifish/harness/test_held_capture_contract.py +50 -0
- package/tools/modules/minifish/harness/test_pty_tail.py +39 -0
- package/tools/modules/minifish/harness/test_runtime_dependencies.py +49 -0
- package/tools/modules/minifish/harness/zero-business-plan.mjs +19 -0
- package/tools/modules/minifish/identity.py +6 -0
- package/tools/modules/minifish/package-lock.json +74 -0
- package/tools/modules/minifish/package.json +10 -0
- package/tools/modules/minifish/sdk/package-lock.json +57745 -0
- package/tools/modules/minifish/sdk/package.json +13 -0
- package/tools/modules/workflow/diff_review.py +74 -0
- package/tools/modules/workflow/iteration.py +175 -0
- package/README.md +0 -3
|
@@ -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
|
package/README.md
DELETED