@dforge-core/dforge-mcp 0.2.45 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/package.json +48 -46
  4. package/scripts/install-skills.mjs +1 -0
  5. package/skills/dforge-1c-import/SKILL.md +374 -0
  6. package/skills/dforge-1c-import/references/environment.md +97 -0
  7. package/skills/dforge-1c-import/references/loading.md +161 -0
  8. package/skills/dforge-1c-import/references/measurements.md +210 -0
  9. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  10. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  11. package/skills/dforge-1c-import/scripts/README.md +110 -0
  12. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  13. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  14. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  15. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  16. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  17. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  18. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  19. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  20. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  21. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  22. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  23. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  24. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  25. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  26. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  27. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  28. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  29. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  30. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  31. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  32. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  33. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  34. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  35. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  36. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  37. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  38. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  39. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  40. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  41. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  42. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  43. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  44. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  45. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  46. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  47. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  48. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  49. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  50. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  51. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  52. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  53. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  54. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  55. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  56. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  57. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  58. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  59. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  60. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  61. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  62. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  63. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  64. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  65. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  66. package/skills/dforge-mcp-author/SKILL.md +5 -0
@@ -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:])