@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -1
  3. package/dist/server.js +71 -3
  4. package/package.json +3 -3
  5. package/resources/docs/dsl-reference.md +1 -1
  6. package/resources/schemas/data-views.schema.json +50 -0
  7. package/scripts/install-skills.mjs +1 -0
  8. package/skills/dforge-1c-import/SKILL.md +375 -0
  9. package/skills/dforge-1c-import/references/environment.md +98 -0
  10. package/skills/dforge-1c-import/references/loading.md +164 -0
  11. package/skills/dforge-1c-import/references/measurements.md +210 -0
  12. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  13. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  14. package/skills/dforge-1c-import/scripts/README.md +164 -0
  15. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  16. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  17. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  18. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  19. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  20. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  21. package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
  22. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  23. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  24. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  25. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  26. package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
  27. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  28. package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
  29. package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
  30. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  31. package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
  32. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  33. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  34. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  35. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  36. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  37. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  38. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  39. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  40. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  41. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  42. package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
  43. package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
  44. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  45. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  46. package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
  61. package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
  62. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  63. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  64. package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
  65. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  66. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  67. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  68. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  69. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  70. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  71. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  72. package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
  73. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  74. package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
  75. package/skills/dforge-mcp-author/SKILL.md +5 -0
  76. package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
  77. package/skills/dforge-mcp-author/references/data-views.md +38 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +32 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +30 -5
  80. package/skills/dforge-mcp-author/references/queries.md +17 -2
  81. package/skills/dforge-mcp-author/references/registers.md +1 -0
  82. package/skills/dforge-mcp-author/references/reports.md +1 -1
  83. package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ u"""The behaviour spec: what each document's posting writes, read out of the code.
4
+
5
+ Reading and generating are two steps with a file between them (docs/task-vendor-logic.md
6
+ §3). This is the reading step. For every movement 1C actually recorded - a (document,
7
+ register) pair in `recorded.json` - it takes the write sites the call graph found behind
8
+ it and writes down, per site, the requirement:
9
+
10
+ row `Запись = Набор.Добавить()`: every column assigned to that record before the next
11
+ `Добавить`, including analytics set through `УстановитьСубконто(Счет, Субконто,
12
+ Вид, Значение)` and whole-record copies through `ЗаполнитьЗначенияСвойств`;
13
+ table a table handed to the set (`Загрузить`, or `мТаблицаДвижений` + `ВыполнитьПриход`):
14
+ the columns filled from the header (`ЗаполнитьЗначения`), the collapse
15
+ (`Свернуть`), the table it was copied from by column name, and where that one
16
+ came from - followed one call up, to the query and the tables it reads.
17
+
18
+ Each entry also carries the conditions it sits under and the loop it runs in, because a
19
+ binding needs both: a condition becomes a filter or a computed column, a loop over a
20
+ tabular section is what "posts from lines" means.
21
+
22
+ Expressions are kept as compact text. They are the requirement as 1C stated it, for a person
23
+ and for the generator to read; none of it is meant to be run, and none of it goes into a
24
+ module (§8).
25
+
26
+ What the reader cannot place is listed in the summary, with counts, instead of dropped.
27
+
28
+ python tools/1c.py spec # every document; out/behaviour/*.json
29
+ python tools/1c.py spec ПриходныйКассовыйОрдер # one document, printed
30
+ """
31
+ from __future__ import print_function
32
+
33
+ import codecs
34
+ import collections
35
+ import io
36
+ import json
37
+ import os
38
+ import re
39
+ import sys
40
+
41
+ import bsl
42
+ import callgraph
43
+ import idioms
44
+ import paths
45
+
46
+ TARGET = os.path.join(paths.OUT, "behaviour")
47
+
48
+ SUBCONTO = bsl.norm(u"УстановитьСубконто")
49
+ ADD_MOVEMENT = bsl.norm(u"ДобавитьДвижение")
50
+ COPY_PROPS = bsl.norm(u"ЗаполнитьЗначенияСвойств")
51
+ TEXT = bsl.norm(u"Текст")
52
+ RECEIPT = frozenset(bsl.norm(x) for x in (u"ВыполнитьПриход", u"ДобавитьПриход",
53
+ u"ВыполнитьДвижениеПриход"))
54
+ EXPENSE = frozenset(bsl.norm(x) for x in (u"ВыполнитьРасход", u"ДобавитьРасход",
55
+ u"ВыполнитьДвижениеРасход"))
56
+ QUERY_SOURCE = re.compile(u"(?:ИЗ|FROM|СОЕДИНЕНИЕ|JOIN)\\s+((?:Документ|Справочник|"
57
+ u"РегистрНакопления|РегистрСведений|РегистрБухгалтерии|"
58
+ u"Document|Catalog|AccumulationRegister|InformationRegister)"
59
+ u"\\.[\\w.]+)", re.U | re.I)
60
+
61
+
62
+ def conditions(context):
63
+ """The conditions a statement sits under, as text, outermost first."""
64
+ found = []
65
+ for frame in context:
66
+ if frame[0] != "if":
67
+ continue
68
+ for earlier in frame[2]:
69
+ found.append(u"НЕ (%s)" % bsl.render(earlier))
70
+ if frame[1] is not None:
71
+ found.append(bsl.render(frame[1]))
72
+ return found
73
+
74
+
75
+ def loop(context):
76
+ """The innermost loop around a statement, as text."""
77
+ for frame in reversed(context):
78
+ if frame[0] == "foreach":
79
+ return u"Для каждого %s Из %s" % (frame[1], bsl.render(frame[2]))
80
+ if frame[0] == "while":
81
+ return u"Пока %s" % bsl.render(frame[1])
82
+ if frame[0] == "for":
83
+ return u"Для %s = %s По %s" % (frame[1], bsl.render(frame[2]), bsl.render(frame[3]))
84
+ return None
85
+
86
+
87
+ def calls_on(expr, receiver):
88
+ """Calls `<receiver>.<method>(...)` anywhere inside an expression."""
89
+ for sub in bsl.subexpressions(expr):
90
+ if sub[0] == "callx" and sub[1][0] == "member" and sub[1][1][0] == "name" \
91
+ and bsl.norm(sub[1][1][1]) == receiver:
92
+ yield bsl.norm(sub[1][2]), sub
93
+
94
+
95
+ def named_call(sub, name):
96
+ callee = sub[1]
97
+ if callee[0] == "name":
98
+ return bsl.norm(callee[1]) == name
99
+ return callee[0] == "member" and bsl.norm(callee[2]) == name
100
+
101
+
102
+ class Reader(object):
103
+
104
+ def __init__(self, modules):
105
+ self.locator = idioms.Locator(modules)
106
+ self.recordsets = dict((bsl.norm(m.object), m) for m in modules if m.kind == "recordset")
107
+ self.missing = collections.Counter()
108
+
109
+ # -- row by row ---------------------------------------------------------------------
110
+
111
+ def row(self, method, line, op):
112
+ """The record added at `line` and every column set on it before the next add."""
113
+ statements = list(bsl.walk(method[5]))
114
+ if op == ADD_MOVEMENT:
115
+ return self.set_properties(statements, line)
116
+ start = None
117
+ record = None
118
+ for index, (st, context) in enumerate(statements):
119
+ if st[1] == line and st[0] == "assign" and st[2][0] == "name":
120
+ for sub in bsl.subexpressions(st[3]):
121
+ # `Набор.Добавить()`, or a bare `Добавить()` inside the set's own module
122
+ if sub[0] == "callx" and named_call(sub, op):
123
+ start, record = index, bsl.norm(st[2][1])
124
+ break
125
+ if start is not None:
126
+ break
127
+ if start is None:
128
+ self.missing["row: record variable not found"] += 1
129
+ return None
130
+ entry = self.row_from(statements, start, record)
131
+ if op in RECEIPT:
132
+ entry["kind"] = "receipt"
133
+ elif op in EXPENSE:
134
+ entry["kind"] = "expense"
135
+ return entry, statements[start][1]
136
+
137
+ def set_properties(self, statements, line):
138
+ """`Набор.Колонка = …; Набор.ДобавитьДвижение()` - the 7.7-style call adds one
139
+ record from the values set on the set itself since its previous add."""
140
+ site = None
141
+ for index, (st, context) in enumerate(statements):
142
+ if st[1] == line and st[0] == "call" and st[2][1][0] == "member" \
143
+ and bsl.norm(st[2][1][2]) == ADD_MOVEMENT:
144
+ site = index
145
+ break
146
+ if site is None:
147
+ self.missing["row: ДобавитьДвижение call not found"] += 1
148
+ return None
149
+ receiver = bsl.render(statements[site][0][2][1][1])
150
+ site_context = statements[site][1]
151
+ begin = 0
152
+ for i in range(site - 1, -1, -1):
153
+ st = statements[i][0]
154
+ if st[0] == "call" and st[2][1][0] == "member" \
155
+ and bsl.norm(st[2][1][2]) == ADD_MOVEMENT \
156
+ and bsl.render(st[2][1][1]) == receiver:
157
+ begin = i + 1
158
+ break
159
+ columns = collections.OrderedDict()
160
+ for st, context in statements[begin:site]:
161
+ if st[0] == "assign" and st[2][0] == "member" and bsl.render(st[2][1]) == receiver:
162
+ extra = conditions(context[len(site_context):]) \
163
+ if context[:len(site_context)] == site_context else conditions(context)
164
+ self._column(columns, st[2][2], st[3], extra)
165
+ return {"shape": "row", "columns": columns, "via": "set properties"}, site_context
166
+
167
+ def row_from(self, statements, start, record, stop=None):
168
+ """The columns set on `record` after statements[start], until it is reassigned."""
169
+ site_context = statements[start][1]
170
+ columns = collections.OrderedDict()
171
+ copies = []
172
+ for st, context in statements[start + 1:stop]:
173
+ if st[0] == "assign" and st[2][0] == "name" and bsl.norm(st[2][1]) == record:
174
+ break # the next record
175
+ extra = conditions(context[len(site_context):])
176
+ if st[0] == "assign" and st[2][0] == "member" and st[2][1][0] == "name" \
177
+ and bsl.norm(st[2][1][1]) == record:
178
+ self._column(columns, st[2][2], st[3], extra)
179
+ continue
180
+ for expr in bsl.expressions(st):
181
+ for sub in bsl.subexpressions(expr):
182
+ if sub[0] != "callx":
183
+ continue
184
+ args = sub[2]
185
+ if named_call(sub, SUBCONTO) and len(args) >= 4 and args[1] is not None \
186
+ and args[1][0] == "member" and args[1][1][0] == "name" \
187
+ and bsl.norm(args[1][1][1]) == record:
188
+ self._column(columns, u"%s[%s]" % (args[1][2], bsl.render(args[2])),
189
+ args[3], extra)
190
+ elif named_call(sub, COPY_PROPS) and args and args[0] is not None \
191
+ and args[0][0] == "name" and bsl.norm(args[0][1]) == record:
192
+ copies.append({"from": bsl.render(args[1]) if len(args) > 1 else u"",
193
+ "only": bsl.render(args[2]) if len(args) > 2 else None,
194
+ "when": extra or None})
195
+ entry = {"shape": "row", "columns": columns}
196
+ if copies:
197
+ entry["copies"] = copies
198
+ return entry
199
+
200
+ @staticmethod
201
+ def _column(columns, name, expr, extra):
202
+ value = {"value": bsl.render(expr)}
203
+ if extra:
204
+ value["when"] = extra
205
+ columns.setdefault(name, []).append(value)
206
+
207
+ # -- a table at once ----------------------------------------------------------------
208
+
209
+ def table(self, module, method, line, op):
210
+ table = self.locator.table_of(method, line, op)
211
+ entry = {"shape": "table"}
212
+ if op in RECEIPT:
213
+ entry["kind"] = "receipt"
214
+ elif op in EXPENSE:
215
+ entry["kind"] = "expense"
216
+ if table is None:
217
+ self.missing["table: no table found"] += 1
218
+ return entry
219
+ entry["table"] = bsl.render(table)
220
+ if table[0] != "name":
221
+ entry["source"] = self.source(module, method, table)
222
+ return entry
223
+ name = bsl.norm(table[1])
224
+ statements = list(bsl.walk(method[5]))
225
+ site = next((i for i, (st, _) in enumerate(statements) if st[1] >= line), len(statements))
226
+ begin = 0
227
+ for i in range(site - 1, -1, -1):
228
+ st = statements[i][0]
229
+ if st[0] == "assign" and st[2][0] == "name" and bsl.norm(st[2][1]) == name:
230
+ begin = i
231
+ break
232
+ window = statements[begin:site]
233
+ fills = collections.OrderedDict()
234
+ per_line = collections.OrderedDict()
235
+ rows = []
236
+ for offset, (st, context) in enumerate(window):
237
+ # a line added to the table and filled column by column
238
+ if st[0] == "assign" and st[2][0] == "name":
239
+ for called, sub in calls_on(st[3], name):
240
+ if called in idioms.ROW_OPS:
241
+ rows.append(self.row_from(statements, begin + offset,
242
+ bsl.norm(st[2][1]), stop=site))
243
+ for expr in bsl.expressions(st):
244
+ for called, sub in calls_on(expr, name):
245
+ args = sub[2]
246
+ if called == idioms.FILL and len(args) >= 2 and args[1] is not None and args[1][0] == "lit":
247
+ fill = {"value": bsl.render(args[0])}
248
+ if conditions(context):
249
+ fill["when"] = conditions(context)
250
+ fills.setdefault(args[1][1], []).append(fill)
251
+ elif called == idioms.COLLAPSE:
252
+ entry["collapse"] = {
253
+ "by": bsl.render(args[0]) if args else u"",
254
+ "sum": bsl.render(args[1]) if len(args) > 1 else u""}
255
+ for sub in bsl.subexpressions(expr):
256
+ if sub[0] == "callx" and named_call(sub, idioms.LOAD_BY_NAME) and len(sub[2]) >= 2 and sub[2][1] is not None and sub[2][1][0] == "name" and bsl.norm(sub[2][1][1]) == name:
257
+ entry.setdefault("from", []).append(
258
+ self.source(module, method, sub[2][0]))
259
+ # a loop over the table setting a column on each line
260
+ if st[0] == "assign" and st[2][0] == "member" and st[2][1][0] == "name":
261
+ for frame in context:
262
+ if frame[0] == "foreach" and bsl.norm(st[2][1][1]) == bsl.norm(frame[1]) and frame[2][0] == "name" and bsl.norm(frame[2][1]) == name:
263
+ per_line.setdefault(st[2][2], []).append({"value": bsl.render(st[3])})
264
+ if rows:
265
+ entry["rows"] = rows
266
+ if fills:
267
+ entry["header"] = fills
268
+ if per_line:
269
+ entry["per_line"] = per_line
270
+ if "from" not in entry and not rows:
271
+ entry["from"] = [self.source(module, method, table)]
272
+ return entry
273
+
274
+ def source(self, module, method, expr, up=True):
275
+ """Where a table comes from: its origin, and for a query the tables it reads;
276
+ for a parameter, the same one call up, in the callers inside the module."""
277
+ origin = self.locator.origin(method, expr)
278
+ found = {"expr": bsl.render(expr), "origin": origin}
279
+ if "query" in origin:
280
+ tables = self.query_tables(method)
281
+ if tables:
282
+ found["reads"] = tables
283
+ if origin == "param" and up and expr is not None and expr[0] == "name":
284
+ index = [bsl.norm(p[0]) for p in method[3]].index(bsl.norm(expr[1]))
285
+ callers = []
286
+ for other in module.methods.values():
287
+ for st in bsl.statements(other[5]):
288
+ for e in bsl.expressions(st):
289
+ for sub in bsl.subexpressions(e):
290
+ if sub[0] == "callx" and sub[1][0] == "name" \
291
+ and bsl.norm(sub[1][1]) == bsl.norm(method[1]) \
292
+ and len(sub[2]) > index:
293
+ callers.append(dict(self.source(module, other, sub[2][index],
294
+ up=False),
295
+ caller=other[1]))
296
+ if callers:
297
+ found["callers"] = callers
298
+ else:
299
+ self.missing["source: parameter with no caller in the module"] += 1
300
+ return found
301
+
302
+ @staticmethod
303
+ def query_tables(method):
304
+ """The tables named in the query texts this method sets."""
305
+ tables = []
306
+ for st in bsl.statements(method[5]):
307
+ if st[0] == "assign" and st[2][0] == "member" and bsl.norm(st[2][2]) == TEXT:
308
+ for sub in bsl.subexpressions(st[3]):
309
+ if sub[0] == "lit" and isinstance(sub[1], type(u"")):
310
+ for match in QUERY_SOURCE.findall(sub[1]):
311
+ if match not in tables:
312
+ tables.append(match)
313
+ return tables
314
+
315
+ # -- one site -----------------------------------------------------------------------
316
+
317
+ def site(self, path, line, op, register=None):
318
+ module, method = self.locator.method_at(path, line)
319
+ if method is None:
320
+ self.missing["site: method not found"] += 1
321
+ return None
322
+ kind = idioms.shape(op)
323
+ own = self.recordsets.get(bsl.norm(register)) if register else None
324
+ if own is not None and op in own.methods and kind != "load":
325
+ # the register's own module defines this method; УТП's write from the table
326
+ # handed over in `мТаблицаДвижений`
327
+ kind = "module"
328
+ where = {"site": u"%s:%d" % (path, line), "in": u"%s.%s" % (module.object, method[1])}
329
+ if kind == "row":
330
+ found = self.row(method, line, op)
331
+ if found is None:
332
+ return dict(where, shape="row", unread=True)
333
+ entry, context = found
334
+ elif kind in ("load", "module"):
335
+ entry = self.table(module, method, line, op)
336
+ context = self._context_at(method, line)
337
+ else:
338
+ return None # `Записать` alone, or a set method
339
+ entry.update(where)
340
+ guards = conditions(context)
341
+ if guards:
342
+ entry["when"] = guards
343
+ around = loop(context)
344
+ if around:
345
+ entry["loop"] = around
346
+ return entry
347
+
348
+ @staticmethod
349
+ def _context_at(method, line):
350
+ for st, context in bsl.walk(method[5]):
351
+ if st[1] == line:
352
+ return context
353
+ return ()
354
+
355
+
356
+ def main():
357
+ args = sys.argv[1:]
358
+ out = codecs.getwriter("utf-8")(sys.stdout.buffer)
359
+ modules, _ = callgraph.load()
360
+ graph = callgraph.Graph(modules, callgraph.register_names())
361
+ graph.run()
362
+ graph.document_writes(callgraph.subscriptions(modules), callgraph.declared_movements())
363
+ with io.open(os.path.join(paths.OUT, "recorded.json"), encoding="utf-8") as f:
364
+ recorded = json.load(f)["documents"]
365
+ reader = Reader(modules)
366
+
367
+ if not os.path.isdir(TARGET):
368
+ os.makedirs(TARGET)
369
+ shapes = collections.Counter()
370
+ written = 0
371
+ for document in sorted(recorded):
372
+ if args and document != args[0]:
373
+ continue
374
+ movements = collections.OrderedDict()
375
+ for register, rows in sorted(recorded[document].items()):
376
+ key = bsl.norm(register)
377
+ entries = []
378
+ for event in ("posting", "on-write", "before-write"):
379
+ for path, line, op in sorted(graph.document_sites.get((document, event, key),
380
+ ())):
381
+ entry = reader.site(path, line, op, register)
382
+ if entry is not None:
383
+ entry["event"] = event
384
+ entries.append(entry)
385
+ shapes[entry.get("shape")] += 1
386
+ movements[register] = {"rows_in_1c": rows, "writes": entries}
387
+ spec = {"document": document, "movements": movements}
388
+ with codecs.open(os.path.join(TARGET, document + ".json"), "w", "utf-8") as f:
389
+ json.dump(spec, f, ensure_ascii=False, indent="\t")
390
+ written += 1
391
+ if args:
392
+ out.write(json.dumps(spec, ensure_ascii=False, indent=2))
393
+ out.write(u"\n")
394
+ out.write(u"%d documents, %s entries; unread: %s\n" % (
395
+ written, dict(shapes), dict(reader.missing) or u"none"))
396
+ out.write(u"written %s\n" % TARGET)
397
+
398
+
399
+ if __name__ == "__main__":
400
+ main()