@dforge-core/dforge-mcp 0.2.45 → 0.2.47
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 +45 -0
- package/README.md +2 -1
- package/dist/server.js +71 -3
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +375 -0
- package/skills/dforge-1c-import/references/environment.md +98 -0
- package/skills/dforge-1c-import/references/loading.md +164 -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 +164 -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/changes.py +326 -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 +770 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +202 -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 +325 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -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/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +68 -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 +91 -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 +493 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -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/store.py +126 -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/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
- package/skills/dforge-mcp-author/SKILL.md +5 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +32 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +1 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Read the 1C forms out of the configuration dump.
|
|
5
|
+
|
|
6
|
+
The port carries every attribute a catalogue declares, because that is what the metadata
|
|
7
|
+
says - and the metadata says nothing about how they are grouped. `Номенклатура` is a flat
|
|
8
|
+
list of 81 attributes, so the card the port generates is a flat list of 81 fields.
|
|
9
|
+
|
|
10
|
+
The grouping is in the forms. An ordinary 1C form is not text: it sits in `<guid>.0.bin`
|
|
11
|
+
in 1C's own container format - a 16-byte header, then a chain of blocks whose 31-byte
|
|
12
|
+
ASCII headers give (data size, page size, next page). The first chain is a directory of
|
|
13
|
+
(attributes address, data address) pairs; each attributes block carries an inner file's
|
|
14
|
+
name and each data block its body. A form container holds two of them, `form` and
|
|
15
|
+
`module` - the control tree and the form's code.
|
|
16
|
+
|
|
17
|
+
Inside `form`, a panel's pages carry their captions:
|
|
18
|
+
|
|
19
|
+
{4, {1,1, {"ru","Цены номенклатуры"}}, ..., -1,1,1,"ЦеныНоменклатуры",1, ...
|
|
20
|
+
|
|
21
|
+
which is where a card's tabs should come from.
|
|
22
|
+
|
|
23
|
+
python tools/forms.py # coverage, and writes out/formtabs.json
|
|
24
|
+
python tools/forms.py <1C object> # that object's forms, and one form's tabs
|
|
25
|
+
|
|
26
|
+
What is NOT here is which page a given field sits on. A page's controls are not nested
|
|
27
|
+
under it: the page list is one node of the panel and the controls are another, tied by
|
|
28
|
+
an id mapping whose layout is not documented and did not yield to three readings of it.
|
|
29
|
+
The configuration is on ordinary forms throughout - 123 of the 182 ported objects have
|
|
30
|
+
only those, and not one has a managed form with anything in it - so there is no clean
|
|
31
|
+
tree to fall back on. The tabs below are therefore a description of the 1C card, not yet
|
|
32
|
+
a placement for ours.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import print_function
|
|
35
|
+
|
|
36
|
+
import codecs
|
|
37
|
+
import collections
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import struct
|
|
42
|
+
import sys
|
|
43
|
+
import zlib
|
|
44
|
+
|
|
45
|
+
import attrs
|
|
46
|
+
import paths
|
|
47
|
+
import meta
|
|
48
|
+
|
|
49
|
+
CONFIG = os.path.join(attrs.OUT, "config")
|
|
50
|
+
GUID = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
51
|
+
|
|
52
|
+
# `{4, {1,1, {"ru","<caption>"}}, ...},-1,1,1,"<name>",1,` - one page of a panel.
|
|
53
|
+
PAGE = re.compile(
|
|
54
|
+
r'\{1,1,\s*\{"ru","([^"\n]{2,60})"\}[\s\S]{0,400}?,-1,1,1,"([A-Za-zЀ-ӿ_][^"\n]{0,60})",1,',
|
|
55
|
+
re.S)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------- container
|
|
59
|
+
|
|
60
|
+
def read_block(buf, at):
|
|
61
|
+
"""One block chain of a 1C container, concatenated."""
|
|
62
|
+
header = buf[at:at + 31]
|
|
63
|
+
if len(header) < 31 or header[:2] != b"\r\n":
|
|
64
|
+
return None
|
|
65
|
+
left = int(header[2:10], 16)
|
|
66
|
+
page, size, following = at, int(header[11:19], 16), int(header[20:28], 16)
|
|
67
|
+
out = b""
|
|
68
|
+
while True:
|
|
69
|
+
chunk = buf[page + 31:page + 31 + size]
|
|
70
|
+
out += chunk[:left]
|
|
71
|
+
left -= min(len(chunk), left)
|
|
72
|
+
if left <= 0 or following == 0x7fffffff:
|
|
73
|
+
break
|
|
74
|
+
header = buf[following:following + 31]
|
|
75
|
+
size, page, following = (int(header[11:19], 16), following,
|
|
76
|
+
int(header[20:28], 16))
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def container_files(path):
|
|
81
|
+
"""The inner files of a 1C container, by name."""
|
|
82
|
+
with open(path, "rb") as f:
|
|
83
|
+
buf = f.read()
|
|
84
|
+
directory = read_block(buf, 16)
|
|
85
|
+
if not directory:
|
|
86
|
+
return {}
|
|
87
|
+
out = {}
|
|
88
|
+
for i in range(0, len(directory) - 11, 12):
|
|
89
|
+
attr_at, data_at, _ = struct.unpack_from("<III", directory, i)
|
|
90
|
+
header = read_block(buf, attr_at)
|
|
91
|
+
if not header or len(header) <= 20:
|
|
92
|
+
continue
|
|
93
|
+
name = header[20:].decode("utf-16-le", "replace").rstrip(u"\x00")
|
|
94
|
+
body = read_block(buf, data_at) or b""
|
|
95
|
+
try:
|
|
96
|
+
body = zlib.decompressobj(-15).decompress(body)
|
|
97
|
+
except zlib.error:
|
|
98
|
+
pass
|
|
99
|
+
out[name] = body
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def form_body(guid, part="form"):
|
|
104
|
+
"""A form's control tree as text, whether it was stored packed or plain."""
|
|
105
|
+
packed = os.path.join(CONFIG, guid + ".0.bin")
|
|
106
|
+
if os.path.isfile(packed):
|
|
107
|
+
body = container_files(packed).get(part)
|
|
108
|
+
return body.decode("utf-8-sig", "replace") if body else None
|
|
109
|
+
plain = os.path.join(CONFIG, guid + ".0.txt")
|
|
110
|
+
if part == "form" and os.path.isfile(plain):
|
|
111
|
+
with codecs.open(plain, "r", "utf-8-sig", "replace") as f:
|
|
112
|
+
return f.read()
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --------------------------------------------------------------------------- forms
|
|
117
|
+
|
|
118
|
+
_ON_DISK = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def on_disk():
|
|
122
|
+
global _ON_DISK
|
|
123
|
+
if _ON_DISK is None:
|
|
124
|
+
_ON_DISK = set(name.split(".")[0] for name in os.listdir(CONFIG))
|
|
125
|
+
return _ON_DISK
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def forms_of(object_guid):
|
|
129
|
+
"""[(1C form name, guid)] for one metadata object, in declaration order."""
|
|
130
|
+
text = meta.load(object_guid) or ""
|
|
131
|
+
found = []
|
|
132
|
+
for guid in dict.fromkeys(re.findall(GUID, text)):
|
|
133
|
+
if guid == object_guid or guid not in on_disk():
|
|
134
|
+
continue
|
|
135
|
+
header = meta.load(guid) or ""
|
|
136
|
+
name = re.search(u'"(Форма[^"]*)"', header)
|
|
137
|
+
if name:
|
|
138
|
+
found.append((name.group(1), guid))
|
|
139
|
+
return found
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def tabs(text):
|
|
143
|
+
"""A panel's pages, in order: [(caption, control name)].
|
|
144
|
+
|
|
145
|
+
1C names a page it was never given a caption for `Страница1`, and a form with one
|
|
146
|
+
such page has no tabs worth porting - that is the whole card.
|
|
147
|
+
"""
|
|
148
|
+
found = []
|
|
149
|
+
for caption, name in PAGE.findall(text or ""):
|
|
150
|
+
if caption.startswith(u"Страница"):
|
|
151
|
+
continue
|
|
152
|
+
found.append((caption, name))
|
|
153
|
+
return found
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def element_form(object_guid):
|
|
157
|
+
"""The form a record opens in, preferring the client's own over the stock one."""
|
|
158
|
+
found = forms_of(object_guid)
|
|
159
|
+
for wanted in (u"ФормаЭлементаМСБ",
|
|
160
|
+
u"ФормаЭлемента",
|
|
161
|
+
u"ФормаДокумента"):
|
|
162
|
+
for name, guid in found:
|
|
163
|
+
if name == wanted:
|
|
164
|
+
return name, guid
|
|
165
|
+
return (found[0] if found else (None, None))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------- reporting
|
|
169
|
+
|
|
170
|
+
def model():
|
|
171
|
+
path = os.path.join(attrs.OUT, "objects_all.json")
|
|
172
|
+
with codecs.open(path, "r", "utf-8") as f:
|
|
173
|
+
doc = json.load(f)
|
|
174
|
+
return doc["objects"] if isinstance(doc, dict) else doc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main():
|
|
178
|
+
rows = model()
|
|
179
|
+
if len(sys.argv) > 1:
|
|
180
|
+
wanted = sys.argv[1].decode("utf-8") if str is bytes else sys.argv[1]
|
|
181
|
+
row = next((r for r in rows if r.get("name") == wanted), None)
|
|
182
|
+
if not row:
|
|
183
|
+
print("no such object: %s" % wanted)
|
|
184
|
+
return 1
|
|
185
|
+
for name, guid in forms_of(row["guid"]):
|
|
186
|
+
text = form_body(guid)
|
|
187
|
+
found = tabs(text)
|
|
188
|
+
print("%-28s %8d chars %d tab(s)"
|
|
189
|
+
% (name, len(text or ""), len(found)))
|
|
190
|
+
for caption, control in found:
|
|
191
|
+
print(" %-42s %s" % (caption, control))
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
# The objects the port actually carries, which is the column map's own list - a
|
|
195
|
+
# tabular section has no form of its own, so only the owners are asked about.
|
|
196
|
+
with codecs.open(paths.derived("columnmap.json"), "r", "utf-8") as f:
|
|
197
|
+
mapping = json.load(f)
|
|
198
|
+
wanted = set()
|
|
199
|
+
for entities in mapping.values():
|
|
200
|
+
for found in entities.values():
|
|
201
|
+
name = (found.get("object") or "").strip()
|
|
202
|
+
if name and " / " not in name:
|
|
203
|
+
wanted.add(name)
|
|
204
|
+
|
|
205
|
+
tally = collections.Counter()
|
|
206
|
+
total_tabs = 0
|
|
207
|
+
found_tabs = collections.OrderedDict()
|
|
208
|
+
for row in rows:
|
|
209
|
+
if row.get("name") not in wanted:
|
|
210
|
+
continue
|
|
211
|
+
name, guid = element_form(row["guid"])
|
|
212
|
+
if not guid:
|
|
213
|
+
tally["no form"] += 1
|
|
214
|
+
continue
|
|
215
|
+
found = tabs(form_body(guid))
|
|
216
|
+
total_tabs += len(found)
|
|
217
|
+
tally["tabs" if found else "one page"] += 1
|
|
218
|
+
if found:
|
|
219
|
+
found_tabs[row["name"]] = {
|
|
220
|
+
"form": name,
|
|
221
|
+
"tabs": [{"caption": caption, "name": control}
|
|
222
|
+
for caption, control in found],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
path = os.path.join(attrs.OUT, "formtabs.json")
|
|
226
|
+
with codecs.open(path, "w", "utf-8") as f:
|
|
227
|
+
f.write(json.dumps(found_tabs, ensure_ascii=False, indent="\t"))
|
|
228
|
+
f.write("\n")
|
|
229
|
+
|
|
230
|
+
print("%-12s %s" % ("ported objects", len(wanted)))
|
|
231
|
+
for key, count in tally.most_common():
|
|
232
|
+
print("%-12s %d" % (key, count))
|
|
233
|
+
print("%-12s %d" % ("tabs found", total_tabs))
|
|
234
|
+
print("\n-> %s" % path)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
sys.exit(main())
|