@dforge-core/dforge-mcp 0.2.44 → 0.2.46-next
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/CHANGELOG.md +73 -0
- package/README.md +3 -2
- package/dist/server.js +109 -84
- package/package.json +48 -46
- package/resources/docs/conventions.md +17 -14
- package/resources/docs/dsl-reference.md +15 -5
- package/resources/schemas/entity.schema.json +1 -1
- package/resources/schemas/registers.schema.json +8 -3
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +374 -0
- package/skills/dforge-1c-import/references/environment.md +97 -0
- package/skills/dforge-1c-import/references/loading.md +161 -0
- package/skills/dforge-1c-import/references/measurements.md +210 -0
- package/skills/dforge-1c-import/references/print-forms.md +148 -0
- package/skills/dforge-1c-import/scripts/1c.py +178 -0
- package/skills/dforge-1c-import/scripts/README.md +110 -0
- package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
- package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
- package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
- package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
- package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
- package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
- package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
- package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
- package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
- package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
- package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
- package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
- package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
- package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
- package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
- package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
- package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
- package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
- package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
- package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
- package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
- package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
- package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
- package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
- package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
- package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
- package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
- package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
- package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
- package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
- package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
- package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
- package/skills/dforge-mcp-author/SKILL.md +8 -2
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
- package/skills/dforge-mcp-author/references/conventions.md +7 -1
- package/skills/dforge-mcp-author/references/field-types.md +3 -2
- package/skills/dforge-mcp-author/references/formulas.md +5 -1
- package/skills/dforge-mcp-author/references/print-templates.md +5 -2
- package/skills/dforge-mcp-author/references/queries.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +18 -3
- package/skills/dforge-mcp-author/references/reports.md +4 -1
- package/skills/dforge-mcp-author/references/schema-import.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +1 -1
- package/skills/dforge-mcp-author/references/translations.md +38 -15
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
- package/skills/dforge-module-build/SKILL.md +10 -1
- package/skills/dforge-module-ship/SKILL.md +31 -3
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Parse the 1C configuration files dumped by dump1c.py into a readable model.
|
|
5
|
+
|
|
6
|
+
The internal format is a nested brace list:
|
|
7
|
+
{2,
|
|
8
|
+
{1e4190e9-...},6,
|
|
9
|
+
{9cd510cd-..., <- metadata class GUID
|
|
10
|
+
{1,
|
|
11
|
+
{50, ... ,"Name",{2,"ru","Synonym","uk","Синонім"}, ...
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
kinds map metadata class GUIDs to the physical table families via DBNames counts
|
|
15
|
+
objects extract name + synonym for every metadata object
|
|
16
|
+
"""
|
|
17
|
+
import codecs
|
|
18
|
+
import collections
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
import paths
|
|
25
|
+
|
|
26
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
OUT = paths.OUT
|
|
28
|
+
CONFIG = os.path.join(OUT, "config")
|
|
29
|
+
|
|
30
|
+
GUID = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --------------------------------------------------------------------------- parser
|
|
34
|
+
|
|
35
|
+
def parse(text):
|
|
36
|
+
"""Nested brace list -> nested Python lists. Strings keep their value, everything
|
|
37
|
+
else stays a bare token (number, GUID, base64 run)."""
|
|
38
|
+
pos = 0
|
|
39
|
+
n = len(text)
|
|
40
|
+
|
|
41
|
+
def skip_ws():
|
|
42
|
+
nonlocal pos
|
|
43
|
+
while pos < n and text[pos] in " \t\r\n":
|
|
44
|
+
pos += 1
|
|
45
|
+
|
|
46
|
+
def read_string():
|
|
47
|
+
nonlocal pos
|
|
48
|
+
pos += 1 # opening quote
|
|
49
|
+
chunks = []
|
|
50
|
+
while pos < n:
|
|
51
|
+
ch = text[pos]
|
|
52
|
+
if ch == '"':
|
|
53
|
+
if pos + 1 < n and text[pos + 1] == '"':
|
|
54
|
+
chunks.append('"')
|
|
55
|
+
pos += 2
|
|
56
|
+
continue
|
|
57
|
+
pos += 1
|
|
58
|
+
break
|
|
59
|
+
chunks.append(ch)
|
|
60
|
+
pos += 1
|
|
61
|
+
return "".join(chunks)
|
|
62
|
+
|
|
63
|
+
def read_list():
|
|
64
|
+
nonlocal pos
|
|
65
|
+
pos += 1 # opening brace
|
|
66
|
+
items = []
|
|
67
|
+
while pos < n:
|
|
68
|
+
skip_ws()
|
|
69
|
+
if pos >= n:
|
|
70
|
+
break
|
|
71
|
+
ch = text[pos]
|
|
72
|
+
if ch == "}":
|
|
73
|
+
pos += 1
|
|
74
|
+
break
|
|
75
|
+
if ch == ",":
|
|
76
|
+
pos += 1
|
|
77
|
+
continue
|
|
78
|
+
items.append(read_value())
|
|
79
|
+
return items
|
|
80
|
+
|
|
81
|
+
def read_value():
|
|
82
|
+
nonlocal pos
|
|
83
|
+
skip_ws()
|
|
84
|
+
ch = text[pos]
|
|
85
|
+
if ch == "{":
|
|
86
|
+
return read_list()
|
|
87
|
+
if ch == '"':
|
|
88
|
+
return read_string()
|
|
89
|
+
start = pos
|
|
90
|
+
while pos < n and text[pos] not in ",{}\r\n":
|
|
91
|
+
pos += 1
|
|
92
|
+
return text[start:pos].strip()
|
|
93
|
+
|
|
94
|
+
skip_ws()
|
|
95
|
+
return read_value() if pos < n else []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load(guid):
|
|
99
|
+
path = os.path.join(CONFIG, guid + ".txt")
|
|
100
|
+
if not os.path.isfile(path):
|
|
101
|
+
return None
|
|
102
|
+
with codecs.open(path, "r", "utf-8-sig", errors="replace") as f:
|
|
103
|
+
return f.read()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def walk(node):
|
|
107
|
+
"""Yield every nested list, depth first."""
|
|
108
|
+
if isinstance(node, list):
|
|
109
|
+
yield node
|
|
110
|
+
for item in node:
|
|
111
|
+
for sub in walk(item):
|
|
112
|
+
yield sub
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --------------------------------------------------------------------------- kinds
|
|
116
|
+
|
|
117
|
+
def collect_guid_groups(tree):
|
|
118
|
+
"""Find '{classGuid, count, guid, guid, ...}': a metadata class and its members."""
|
|
119
|
+
groups = []
|
|
120
|
+
for node in walk(tree):
|
|
121
|
+
if len(node) < 3:
|
|
122
|
+
continue
|
|
123
|
+
class_guid, count = node[0], node[1]
|
|
124
|
+
if not isinstance(class_guid, str) or not GUID.match(class_guid):
|
|
125
|
+
continue
|
|
126
|
+
if not isinstance(count, str) or not count.isdigit():
|
|
127
|
+
continue
|
|
128
|
+
members = [g for g in node[2:] if isinstance(g, str) and GUID.match(g)]
|
|
129
|
+
if members and len(members) == int(count):
|
|
130
|
+
groups.append({"classGuid": class_guid, "members": members})
|
|
131
|
+
return groups
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def dbnames_index():
|
|
135
|
+
"""A GUID appears once per physical table it owns: the real one plus change-tracking
|
|
136
|
+
(_..ChngR) and totals (_..T) companions. Keep the primary table."""
|
|
137
|
+
with codecs.open(os.path.join(OUT, "dbnames.json"), "r", "utf-8") as f:
|
|
138
|
+
entries = json.load(f)["entries"]
|
|
139
|
+
index = {}
|
|
140
|
+
for e in entries:
|
|
141
|
+
kind = e["type"]
|
|
142
|
+
if kind.endswith("ChngR") or kind.endswith("Recalc"):
|
|
143
|
+
index.setdefault(e["guid"], e) # only if nothing better shows up
|
|
144
|
+
else:
|
|
145
|
+
prev = index.get(e["guid"])
|
|
146
|
+
if prev is None or prev["type"].endswith("ChngR") or prev["type"].endswith("Recalc"):
|
|
147
|
+
index[e["guid"]] = e
|
|
148
|
+
return index
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_kinds(args):
|
|
152
|
+
"""Each group's family is whatever DBNames calls its members - no guessing from counts."""
|
|
153
|
+
tree = parse(load("1e4190e9-76c2-456e-a607-4d817110ffd9") or "")
|
|
154
|
+
groups = collect_guid_groups(tree)
|
|
155
|
+
dbn = dbnames_index()
|
|
156
|
+
|
|
157
|
+
print("%-40s %7s %-18s %s" % ("metadata class GUID", "members", "family", "resolved"))
|
|
158
|
+
print("-" * 84)
|
|
159
|
+
mapping = {}
|
|
160
|
+
for g in sorted(groups, key=lambda g: -len(g["members"])):
|
|
161
|
+
families = collections.Counter(dbn[m]["type"] for m in g["members"] if m in dbn)
|
|
162
|
+
family = families.most_common(1)[0][0] if families else None
|
|
163
|
+
resolved = sum(families.values())
|
|
164
|
+
if family:
|
|
165
|
+
mapping[g["classGuid"]] = {"family": family, "members": g["members"]}
|
|
166
|
+
print("%-40s %7d %-18s %d" % (g["classGuid"], len(g["members"]), family or "-", resolved))
|
|
167
|
+
|
|
168
|
+
with codecs.open(os.path.join(OUT, "kinds.json"), "w", "utf-8") as f:
|
|
169
|
+
json.dump(mapping, f, ensure_ascii=False, indent="\t")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# --------------------------------------------------------------------------- objects
|
|
173
|
+
|
|
174
|
+
NAME_AFTER_GUID = re.compile(r'\{0,\s*\d+,\s*[0-9a-fA-F-]{36}\},\s*"([^"]*)",')
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def object_header(text):
|
|
178
|
+
"""Name and ru/uk synonyms sit right after the object's own identity block."""
|
|
179
|
+
m = NAME_AFTER_GUID.search(text)
|
|
180
|
+
if not m:
|
|
181
|
+
return None, {}
|
|
182
|
+
name = m.group(1)
|
|
183
|
+
syn = {}
|
|
184
|
+
tail = text[m.end():m.end() + 1200]
|
|
185
|
+
# `{N,"ru","...","uk","..."}` - read exactly N pairs. Scanning on past them picked up the
|
|
186
|
+
# first attribute's synonym whenever one sat within reach, so a register was named after
|
|
187
|
+
# its resource ("Стоимость") rather than itself.
|
|
188
|
+
sm = re.match(r'\s*\{(\d+),', tail)
|
|
189
|
+
if sm:
|
|
190
|
+
pos = sm.end()
|
|
191
|
+
for _ in range(int(sm.group(1))):
|
|
192
|
+
pm = PAIR.match(tail, pos)
|
|
193
|
+
if not pm:
|
|
194
|
+
break
|
|
195
|
+
syn[pm.group(1)] = pm.group(2).replace('""', '"')
|
|
196
|
+
pos = pm.end()
|
|
197
|
+
return name, syn
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
PAIR = re.compile(r'\s*,?\s*"([a-z]{2})",\s*"((?:[^"]|"")*)"')
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_objects(args):
|
|
204
|
+
with codecs.open(os.path.join(OUT, "kinds.json"), "r", "utf-8") as f:
|
|
205
|
+
kinds = json.load(f)
|
|
206
|
+
result = []
|
|
207
|
+
for class_guid, info in kinds.items():
|
|
208
|
+
for guid in info["members"]:
|
|
209
|
+
text = load(guid)
|
|
210
|
+
if text is None:
|
|
211
|
+
result.append({"guid": guid, "family": info["family"], "name": None, "missing": True})
|
|
212
|
+
continue
|
|
213
|
+
name, syn = object_header(text)
|
|
214
|
+
result.append({"guid": guid, "family": info["family"], "name": name,
|
|
215
|
+
"synonym": syn.get("ru") or syn.get("uk"), "size": len(text)})
|
|
216
|
+
with codecs.open(os.path.join(OUT, "objects.json"), "w", "utf-8") as f:
|
|
217
|
+
json.dump(result, f, ensure_ascii=False, indent="\t")
|
|
218
|
+
named = [r for r in result if r.get("name")]
|
|
219
|
+
print("objects=%d named=%d" % (len(result), len(named)))
|
|
220
|
+
for r in named[:int(args[0]) if args else 20]:
|
|
221
|
+
print(" %-18s %-45s %s" % (r["family"], r["name"], r.get("synonym") or ""))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --------------------------------------------------------------------------- report
|
|
225
|
+
|
|
226
|
+
def cmd_report(args):
|
|
227
|
+
"""Named metadata joined to its physical table and row counts: the porting worklist."""
|
|
228
|
+
with codecs.open(os.path.join(OUT, "objects.json"), "r", "utf-8") as f:
|
|
229
|
+
objects = json.load(f)
|
|
230
|
+
with codecs.open(os.path.join(OUT, "schema.json"), "r", "utf-8") as f:
|
|
231
|
+
tables = {t["name"]: t for t in json.load(f)["tables"]}
|
|
232
|
+
dbn = dbnames_index()
|
|
233
|
+
|
|
234
|
+
rows = []
|
|
235
|
+
for obj in objects:
|
|
236
|
+
entry = dbn.get(obj["guid"])
|
|
237
|
+
physical = entry["physical"] if entry else None
|
|
238
|
+
table = tables.get(physical) if physical else None
|
|
239
|
+
child = sum((t["rows"] or 0) for name, t in tables.items()
|
|
240
|
+
if physical and name.startswith(physical + "_"))
|
|
241
|
+
rows.append({
|
|
242
|
+
"family": entry["type"] if entry else obj["family"],
|
|
243
|
+
"name": obj["name"],
|
|
244
|
+
"synonym": obj.get("synonym"),
|
|
245
|
+
"physical": physical,
|
|
246
|
+
"rows": (table["rows"] or 0) if table else None,
|
|
247
|
+
"childRows": child,
|
|
248
|
+
"columns": len(table["columns"]) if table else None,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
with codecs.open(os.path.join(OUT, "report.json"), "w", "utf-8") as f:
|
|
252
|
+
json.dump(rows, f, ensure_ascii=False, indent="\t")
|
|
253
|
+
|
|
254
|
+
lines = []
|
|
255
|
+
families = args[0].split(",") if args else ["Document", "Reference", "AccumRg", "InfoRg"]
|
|
256
|
+
for family in families:
|
|
257
|
+
members = [r for r in rows if r["family"] == family]
|
|
258
|
+
used = [r for r in members if (r["rows"] or 0) or r["childRows"]]
|
|
259
|
+
used.sort(key=lambda r: -((r["rows"] or 0) + r["childRows"]))
|
|
260
|
+
lines.append("")
|
|
261
|
+
lines.append("## %s - %d objects, %d with data" % (family, len(members), len(used)))
|
|
262
|
+
lines.append("")
|
|
263
|
+
lines.append("| name | synonym | table | rows | child rows | cols |")
|
|
264
|
+
lines.append("|---|---|---|---:|---:|---:|")
|
|
265
|
+
for r in used:
|
|
266
|
+
lines.append("| %s | %s | %s | %s | %s | %s |" % (
|
|
267
|
+
r["name"], r["synonym"] or "", r["physical"],
|
|
268
|
+
"{:,}".format(r["rows"] or 0), "{:,}".format(r["childRows"]), r["columns"]))
|
|
269
|
+
text = "\n".join(lines)
|
|
270
|
+
with codecs.open(os.path.join(OUT, "report.md"), "w", "utf-8") as f:
|
|
271
|
+
f.write(text)
|
|
272
|
+
print("wrote out/report.json and out/report.md (%d objects)" % len(rows))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cmd_allobjects(args):
|
|
276
|
+
"""Every metadata object in the root, not only the ones with a physical table.
|
|
277
|
+
|
|
278
|
+
`objects` is driven by kinds.json, which only keeps classes DBNames knows about, so
|
|
279
|
+
common modules, roles and subsystems are missing from it - and they hold most of the
|
|
280
|
+
configuration's code.
|
|
281
|
+
"""
|
|
282
|
+
tree = parse(load("1e4190e9-76c2-456e-a607-4d817110ffd9") or "")
|
|
283
|
+
dbn = dbnames_index()
|
|
284
|
+
result = []
|
|
285
|
+
for group in collect_guid_groups(tree):
|
|
286
|
+
families = collections.Counter(dbn[m]["type"] for m in group["members"] if m in dbn)
|
|
287
|
+
family = families.most_common(1)[0][0] if families else None
|
|
288
|
+
for guid in group["members"]:
|
|
289
|
+
text = load(guid)
|
|
290
|
+
name, syn = object_header(text) if text is not None else (None, {})
|
|
291
|
+
result.append({
|
|
292
|
+
"guid": guid,
|
|
293
|
+
"classGuid": group["classGuid"],
|
|
294
|
+
"family": family,
|
|
295
|
+
"name": name,
|
|
296
|
+
"synonym": syn.get("ru") or syn.get("uk"),
|
|
297
|
+
})
|
|
298
|
+
with codecs.open(os.path.join(OUT, "objects_all.json"), "w", "utf-8") as f:
|
|
299
|
+
json.dump(result, f, ensure_ascii=False, indent="\t")
|
|
300
|
+
|
|
301
|
+
by_class = collections.Counter()
|
|
302
|
+
for row in result:
|
|
303
|
+
by_class[(row["classGuid"], row["family"])] += 1
|
|
304
|
+
print("objects=%d named=%d" % (len(result), sum(1 for r in result if r["name"])))
|
|
305
|
+
for (class_guid, family), n in by_class.most_common(12):
|
|
306
|
+
sample = next(r["name"] for r in result
|
|
307
|
+
if r["classGuid"] == class_guid and r["name"])
|
|
308
|
+
print(" %-40s %-12s %5d e.g. %s" % (class_guid, family or "-", n, sample))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
COMMANDS = {"kinds": cmd_kinds, "objects": cmd_objects, "report": cmd_report,
|
|
312
|
+
"allobjects": cmd_allobjects}
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
COMMANDS[sys.argv[1]](sys.argv[2:])
|