@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/dist/server.js +109 -84
  4. package/package.json +48 -46
  5. package/resources/docs/conventions.md +17 -14
  6. package/resources/docs/dsl-reference.md +15 -5
  7. package/resources/schemas/entity.schema.json +1 -1
  8. package/resources/schemas/registers.schema.json +8 -3
  9. package/scripts/install-skills.mjs +1 -0
  10. package/skills/dforge-1c-import/SKILL.md +374 -0
  11. package/skills/dforge-1c-import/references/environment.md +97 -0
  12. package/skills/dforge-1c-import/references/loading.md +161 -0
  13. package/skills/dforge-1c-import/references/measurements.md +210 -0
  14. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  15. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  16. package/skills/dforge-1c-import/scripts/README.md +110 -0
  17. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  18. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  19. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  20. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  21. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  22. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  23. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  24. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  25. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  26. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  27. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  28. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  29. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  30. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  31. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  32. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  33. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  34. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  35. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  36. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  37. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  38. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  39. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  40. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  41. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  42. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  43. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  44. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  45. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  46. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -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 +94 -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 +488 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  61. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  62. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  63. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  64. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  65. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  66. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  67. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  68. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  69. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  70. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  71. package/skills/dforge-mcp-author/SKILL.md +8 -2
  72. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
  73. package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
  74. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
  75. package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
  76. package/skills/dforge-mcp-author/references/conventions.md +7 -1
  77. package/skills/dforge-mcp-author/references/field-types.md +3 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +5 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +5 -2
  80. package/skills/dforge-mcp-author/references/queries.md +1 -1
  81. package/skills/dforge-mcp-author/references/registers.md +18 -3
  82. package/skills/dforge-mcp-author/references/reports.md +4 -1
  83. package/skills/dforge-mcp-author/references/schema-import.md +1 -1
  84. package/skills/dforge-mcp-author/references/security.md +1 -1
  85. package/skills/dforge-mcp-author/references/translations.md +38 -15
  86. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
  87. package/skills/dforge-module-build/SKILL.md +10 -1
  88. package/skills/dforge-module-ship/SKILL.md +31 -3
  89. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ The configuration's own division into subsystems, and what each one holds.
5
+
6
+ Where module boundaries come from, when the configuration is not this one. Porting the
7
+ client's own development needed no such thing - 195 objects all prefixed `мсб` or `рз`,
8
+ grouped by reading them - but that grouping is a judgement about THIS infobase and is
9
+ worth nothing to the next one. A 1C configuration already carries a division its authors
10
+ made: подсистемы. Reading it costs nothing and applies to every configuration there is.
11
+
12
+ python tools/1c.py subsystems # every subsystem, with what it holds
13
+ python tools/1c.py subsystems --objects # each one's data-bearing objects
14
+ python tools/1c.py subsystems --name <name> # one subsystem in full
15
+ python tools/1c.py subsystems --overlap # objects claimed by more than one
16
+ python tools/1c.py subsystems --orphans # objects NO subsystem lists
17
+ python tools/1c.py subsystems --json # -> out/subsystems.json
18
+ python tools/1c.py subsystems --contours # -> out/contours_subsystems.py
19
+ python tools/1c.py subsystems --contours --acyclic # ... and make it installable
20
+
21
+ What it does NOT do is decide anything. Subsystem membership is presentation in 1C - an
22
+ object appears in the command interface of every subsystem that lists it - so overlap is
23
+ normal and a module boundary still needs a rule for it. The rule belongs to whoever is
24
+ converting; this reports the facts it would be made from.
25
+ """
26
+ from __future__ import print_function
27
+
28
+ import codecs
29
+ import collections
30
+ import io
31
+ import json
32
+ import os
33
+ import re
34
+ import sys
35
+
36
+ import meta
37
+ import naming
38
+ import paths
39
+
40
+ OUT = paths.OUT
41
+
42
+ # The metadata class of a Подсистема. Class GUIDs are the configuration format's own and
43
+ # are the same in every 1C:Enterprise 8 configuration, which is what makes this portable.
44
+ SUBSYSTEM = "37f2fa9a-b276-11d4-9435-004095e12fc7"
45
+
46
+ # Families that hold rows. Everything else a subsystem lists - reports, roles, icons,
47
+ # constants, common modules - is real content but not something a conversion loads.
48
+ DATA_FAMILIES = ("Reference", "Document", "InfoRg", "AccumRg", "AccRg", "Acc", "Chrc",
49
+ "CKinds", "Enum", "DocumentJournal", "Seq", "Node", "Task", "CRg", "BPr")
50
+
51
+ GUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
52
+
53
+
54
+ def _load(name):
55
+ path = os.path.join(OUT, name)
56
+ if not os.path.isfile(path):
57
+ raise SystemExit("subsystems: %s is missing - run `1c.py dump1c` and `meta` first"
58
+ % path)
59
+ with codecs.open(path, "r", "utf-8") as f:
60
+ return json.load(f)
61
+
62
+
63
+ def _guids_in(tree, known):
64
+ """Every known object GUID mentioned in a metadata tree, in the order it appears.
65
+
66
+ A subsystem's file is a flat list of the GUIDs it contains plus its own identity and
67
+ presentation. Rather than decode the layout, take every GUID that names an object we
68
+ know - a subsystem cannot mention an object it does not contain, and the two GUIDs
69
+ that are its own are dropped by the caller.
70
+ """
71
+ found = []
72
+ seen = set()
73
+
74
+ def walk(node):
75
+ if isinstance(node, list):
76
+ for item in node:
77
+ walk(item)
78
+ elif isinstance(node, str) and node not in seen and GUID.match(node):
79
+ seen.add(node)
80
+ if node in known:
81
+ found.append(node)
82
+
83
+ walk(tree)
84
+ return found
85
+
86
+
87
+ def collect():
88
+ """One record per subsystem: what it contains, and how much of it holds rows."""
89
+ objects = _load("objects_all.json")
90
+ by_guid = dict((o["guid"], o) for o in objects)
91
+
92
+ rows_by_name = {}
93
+ report_path = os.path.join(OUT, "report.json")
94
+ if os.path.isfile(report_path):
95
+ for row in _load("report.json"):
96
+ if row.get("name"):
97
+ rows_by_name[row["name"]] = ((row.get("rows") or 0)
98
+ + (row.get("childRows") or 0))
99
+
100
+ ported = set()
101
+ colmap_path = os.path.join(OUT, "columnmap.json")
102
+ if os.path.isfile(colmap_path):
103
+ mapped = _load("columnmap.json")
104
+ for module in mapped:
105
+ for entity in mapped[module]:
106
+ record = mapped[module][entity]
107
+ if isinstance(record, dict) and record.get("table"):
108
+ ported.add(record["table"])
109
+ table_of = dict((r["name"], r.get("physical")) for r in _load("report.json")
110
+ if r.get("name"))
111
+
112
+ found = []
113
+ for obj in objects:
114
+ if obj.get("classGuid") != SUBSYSTEM:
115
+ continue
116
+ text = meta.load(obj["guid"])
117
+ if text is None:
118
+ continue
119
+ contents = [g for g in _guids_in(meta.parse(text), by_guid) if g != obj["guid"]]
120
+ holds = []
121
+ for guid in contents:
122
+ other = by_guid[guid]
123
+ family = other.get("family")
124
+ if family not in DATA_FAMILIES:
125
+ continue
126
+ name = other["name"]
127
+ holds.append({
128
+ "name": name,
129
+ "family": family,
130
+ "rows": rows_by_name.get(name, 0),
131
+ "ported": table_of.get(name) in ported,
132
+ })
133
+ found.append({
134
+ "name": obj["name"],
135
+ "synonym": obj.get("synonym") or "",
136
+ "guid": obj["guid"],
137
+ "members": len(contents),
138
+ "objects": holds,
139
+ "rows": sum(h["rows"] for h in holds),
140
+ "ported": sum(1 for h in holds if h["ported"]),
141
+ })
142
+ found.sort(key=lambda s: -s["rows"])
143
+ return found
144
+
145
+
146
+ def table(found):
147
+ print("%-46s %7s %7s %7s %14s"
148
+ % ("subsystem", "members", "data", "ported", "rows"))
149
+ print("-" * 86)
150
+ for sub in found:
151
+ print("%-46s %7d %7d %7d %14d"
152
+ % ((sub["synonym"] or sub["name"])[:46], sub["members"],
153
+ len(sub["objects"]), sub["ported"], sub["rows"]))
154
+ print("-" * 86)
155
+ print("%-46s %7s %7d %7d %14d"
156
+ % ("%d subsystem(s)" % len(found), "",
157
+ sum(len(s["objects"]) for s in found),
158
+ sum(s["ported"] for s in found),
159
+ sum(s["rows"] for s in found)))
160
+ print("\nRows are counted per subsystem, so an object in two subsystems is counted\n"
161
+ "twice - `--overlap` says how much of that there is.")
162
+
163
+
164
+ def detail(found, name):
165
+ wanted = [s for s in found
166
+ if s["name"] == name or s["synonym"] == name
167
+ or s["name"].lower().startswith(name.lower())]
168
+ if not wanted:
169
+ raise SystemExit("subsystems: no subsystem matches %r" % name)
170
+ for sub in wanted:
171
+ print("%s %s" % (sub["name"], sub["synonym"]))
172
+ print(" %d member(s), %d of them hold rows, %d already ported, %d rows"
173
+ % (sub["members"], len(sub["objects"]), sub["ported"], sub["rows"]))
174
+ for held in sorted(sub["objects"], key=lambda h: -h["rows"]):
175
+ print(" %-16s %-46s %12d %s"
176
+ % (held["family"], held["name"][:46], held["rows"],
177
+ "ported" if held["ported"] else ""))
178
+
179
+
180
+ def overlap(found):
181
+ """An object listed by more than one subsystem, which is normal and is the reason a
182
+ module boundary needs a rule rather than a lookup."""
183
+ owners = collections.OrderedDict()
184
+ rows = {}
185
+ for sub in found:
186
+ for held in sub["objects"]:
187
+ owners.setdefault(held["name"], []).append(sub["name"])
188
+ rows[held["name"]] = held["rows"]
189
+ shared = [(n, o) for n, o in owners.items() if len(o) > 1]
190
+ shared.sort(key=lambda p: (-len(p[1]), -rows[p[0]]))
191
+ print("%d data object(s) in a subsystem; %d are in more than one"
192
+ % (len(owners), len(shared)))
193
+ print("%d row(s) once each; %d counted more than once"
194
+ % (sum(rows.values()), sum(rows[n] for n, _ in shared)))
195
+ if not shared:
196
+ return
197
+ print("\nthe 25 most shared:")
198
+ for name, who in shared[:25]:
199
+ print(" %-46s %12d %d: %s"
200
+ % (name[:46], rows[name], len(who), ", ".join(who[:4])))
201
+
202
+
203
+ def orphans(found):
204
+ """Objects that hold rows and that no subsystem lists.
205
+
206
+ The reason this view exists rather than being assumed away: subsystem membership is
207
+ optional in 1C, so a conversion that walked the subsystems alone would drop whatever
208
+ nobody bothered to file - silently, and in proportion to how carelessly the
209
+ configuration was maintained. Counting them is how you learn whether the subsystem
210
+ tree is a usable spine for this configuration or only a partial one.
211
+ """
212
+ claimed = set()
213
+ for sub in found:
214
+ for held in sub["objects"]:
215
+ claimed.add(held["name"])
216
+
217
+ loose = []
218
+ for row in _load("report.json"):
219
+ name = row.get("name")
220
+ rows = (row.get("rows") or 0) + (row.get("childRows") or 0)
221
+ if name and name not in claimed and rows:
222
+ loose.append((rows, row.get("family") or "?", name))
223
+ loose.sort(reverse=True)
224
+ print("%d object(s) hold rows and are in no subsystem, %d rows"
225
+ % (len(loose), sum(l[0] for l in loose)))
226
+ if not loose:
227
+ return
228
+ print("\nthe 30 biggest:")
229
+ for rows, family, name in loose[:30]:
230
+ print(" %-16s %-46s %12d" % (family, name[:46], rows))
231
+
232
+ # Objects a subsystem lists that a conversion has no business generating an entity for.
233
+ # Enums become dropdown options on the fields that cite them; a document journal is 1C's
234
+ # own index over documents and holds nothing of its own; a sequence is a posting
235
+ # watermark. Each is a decision this tool can make because it follows from what the
236
+ # family IS, not from what this configuration does with it.
237
+ SKIP_FAMILIES = ("Enum", "DocumentJournal", "Seq")
238
+
239
+
240
+ def _crossing(shared_code):
241
+ """Entities that something outside their own module references, and everything those
242
+ reach in turn.
243
+
244
+ The subsystem rule alone produced a map that GENERATED and VALIDATED cleanly and could
245
+ not be installed: 16 of 22 modules sat in dependency cycles, because a module holding
246
+ shared objects still points at entities in the modules that share them. Merging every
247
+ cycle would collapse the map back into one module, which is the answer nobody wants.
248
+
249
+ So: every entity referenced from outside its module moves to the shared one, and so
250
+ does everything those entities reach. Every cross-module edge then ends in the shared
251
+ module, the graph is a star, and a star has no cycles by shape rather than by luck.
252
+
253
+ It needs the generated modules to read the references from, which is why this is a
254
+ second pass and not a cleverer first one.
255
+ """
256
+ import columnmap
257
+
258
+ owner, refs, source = {}, collections.defaultdict(set), {}
259
+ generated, mapped = columnmap.build()
260
+ for module in mapped:
261
+ for entity in mapped[module]:
262
+ record = mapped[module][entity]
263
+ if isinstance(record, dict) and record.get("object"):
264
+ source["%s.%s" % (module, entity)] = record["object"]
265
+
266
+ if not os.path.isdir(paths.MODULES):
267
+ raise SystemExit("subsystems: --acyclic needs the modules generated first - run "
268
+ "`1c.py autospec` and `genmodule` against this project")
269
+ for module in sorted(os.listdir(paths.MODULES)):
270
+ folder = os.path.join(paths.MODULES, module, "entities")
271
+ if not os.path.isdir(folder):
272
+ continue
273
+ for name in sorted(os.listdir(folder)):
274
+ if name.endswith(".json"):
275
+ owner["%s.%s" % (module, name[:-5])] = module
276
+ for key in list(owner):
277
+ module = owner[key]
278
+ path = os.path.join(paths.MODULES, module, "entities",
279
+ key.split(".", 1)[1] + ".json")
280
+ with io.open(path, encoding="utf-8") as f:
281
+ spec = json.load(f)
282
+ for field in (spec.get("fields") or {}).values():
283
+ target = (field.get("link") or {}).get("entity")
284
+ if not target:
285
+ continue
286
+ if "." not in target:
287
+ target = "%s.%s" % (module, target)
288
+ if target in owner:
289
+ refs[key].add(target)
290
+
291
+ pulled = set()
292
+ for key, targets in refs.items():
293
+ for target in targets:
294
+ if owner[target] != owner[key]:
295
+ pulled.add(target)
296
+ growing = True
297
+ while growing:
298
+ growing = False
299
+ for key in list(pulled):
300
+ for target in refs.get(key, ()):
301
+ if target not in pulled and owner[target] != shared_code:
302
+ pulled.add(target)
303
+ growing = True
304
+ # A line entity has no 1C object of its own - it is a tabular section of its parent -
305
+ # so moving it means moving the parent, which the source map already resolves to.
306
+ return set(source[k] for k in pulled if k in source)
307
+
308
+
309
+ def _already_ported():
310
+ """1C object names the FIRST project already copies, which this one must leave alone.
311
+
312
+ Subsystem membership does not care who has already ported what, so the standard
313
+ configuration's subsystems duly listed 102 objects the client port already carries -
314
+ Добавления МСБ is the client's own development, and `common` picked up СерииНоменклатуры
315
+ and ЕдиницыИзмерения, which are `masterdata` already. Generating them a second time is not
316
+ redundancy to be tidied later: loading both maps would put 4,932,617 rows into a tenant
317
+ twice, in two schemas, with nothing relating the copies and every reference pointing at
318
+ one of them.
319
+
320
+ The first project's column map is the register of what it carries, so that is what is
321
+ asked. Returns nothing when this IS the first project, since a project cannot be told
322
+ to leave its own work alone.
323
+ """
324
+ if os.path.basename(paths.PROJECT) == "project":
325
+ return set()
326
+ path = os.path.join(OUT, "columnmap.json")
327
+ if not os.path.isfile(path):
328
+ return set()
329
+ with codecs.open(path, "r", "utf-8") as f:
330
+ mapped = json.load(f)
331
+ found = set()
332
+ for module in mapped:
333
+ for entity in mapped[module]:
334
+ record = mapped[module][entity]
335
+ if isinstance(record, dict) and record.get("object"):
336
+ found.add(record["object"])
337
+ return found
338
+
339
+ def emit_contours(found, shared_code="common", acyclic=False):
340
+ """Write a CONTOURS map, one module per subsystem, ready for a project directory.
341
+
342
+ `binding.py` already lets the library be pointed at a different infobase by pointing it
343
+ at a different project directory. What had no answer was where that directory's contour
344
+ map comes from: the one for this client was written by reading 195 objects, which does
345
+ not scale to 524 and does not transfer to the next configuration at all.
346
+
347
+ **The overlap rule is the only judgement here, and it is deliberately the dull one.**
348
+ 194 of the 524 objects are listed by more than one subsystem - `Хозрасчетный` by twelve
349
+ - because membership in 1C is presentation: an object shows up in the command interface
350
+ of every subsystem that lists it. Putting such an object in one of them arbitrarily
351
+ would make the other modules depend on a module named after someone else's concern, and
352
+ the dependency graph would acquire cycles the moment two subsystems shared in both
353
+ directions. So everything shared goes to one module that the others depend on. That is
354
+ what `masterdata` already is in the hand-written port, arrived at the same way.
355
+ """
356
+ owners = collections.OrderedDict()
357
+ for sub in found:
358
+ for held in sub["objects"]:
359
+ if held["family"] in SKIP_FAMILIES:
360
+ continue
361
+ owners.setdefault(held["name"], []).append(sub)
362
+
363
+ crossing = _crossing(shared_code) if acyclic else set()
364
+ mine = _already_ported()
365
+ assigned = collections.OrderedDict()
366
+ skipped = 0
367
+ for name, subs in owners.items():
368
+ if name in mine:
369
+ skipped += 1
370
+ continue
371
+ shared = len(subs) > 1 or name in crossing
372
+ code = shared_code if shared else naming.code(subs[0]["name"])
373
+ assigned.setdefault(code, []).append(name)
374
+ if skipped:
375
+ print(" %d object(s) left to the first project, which already ports them" % skipped)
376
+
377
+ synonym = dict((naming.code(s["name"]), s["synonym"] or s["name"]) for s in found)
378
+ synonym[shared_code] = "Shared across subsystems"
379
+
380
+ lines = [u"# -*- coding: utf-8 -*-",
381
+ u'"""Contour map generated from the configuration\'s own subsystems.',
382
+ u"",
383
+ u"Generated by `1c.py subsystems --contours`; edit it or regenerate it, but do",
384
+ u"not do both and expect the edits to survive.",
385
+ u"",
386
+ u"A subsystem is how the configuration's authors arranged their COMMAND",
387
+ u"INTERFACE. That is a good first guess at a module boundary and no more than a",
388
+ u"guess: it says what its authors thought belonged together on a menu, which is",
389
+ u"usually but not always what belongs together in a schema.",
390
+ u"",
391
+ u"Two things to know before trusting it:",
392
+ u"",
393
+ u" * every object listed by more than one subsystem is in %r, which the rest"
394
+ % shared_code,
395
+ u" depend on. Assigning such an object to one of its subsystems arbitrarily",
396
+ u" would make the others depend on a module named after someone else's",
397
+ u" concern, and two subsystems sharing in both directions would put a cycle",
398
+ u" in the graph.",
399
+ u" * objects in NO subsystem are absent from here entirely. Run",
400
+ u" `1c.py subsystems --orphans` before assuming this map is the whole",
401
+ u" infobase - in the configuration it was generated from it was not close.",
402
+ u"",
403
+ u"The module codes are transliterated from the subsystem names and are meant to",
404
+ u"be renamed by hand; nothing downstream depends on their spelling.",
405
+ u'"""',
406
+ u"import collections",
407
+ u"",
408
+ u"CONTOURS = collections.OrderedDict([",
409
+ u""]
410
+ for code in sorted(assigned):
411
+ label = synonym.get(code, code)
412
+ lines.append(u'\t("%s", {' % code)
413
+ lines.append(u'\t\t"label": u"%s",' % label)
414
+ lines.append(u'\t\t"description": u"%s",' % label)
415
+ lines.append(u'\t\t"color": "#78909C",')
416
+ lines.append(u'\t\t"icon": "box",')
417
+ lines.append(u'\t\t"tags": [],')
418
+ lines.append(u'\t\t"objects": [')
419
+ for name in sorted(assigned[code]):
420
+ lines.append(u'\t\t\tu"%s",' % name)
421
+ lines.append(u"\t\t],")
422
+ lines.append(u"\t}),")
423
+ lines.append(u"")
424
+ lines.append(u"])")
425
+ lines.append(u"")
426
+ lines.append(u"# Object name -> the entity code to use instead of the derived one.")
427
+ lines.append(u"CODES = {}")
428
+ lines.append(u"")
429
+ lines.append(u"# (module, entity, column) -> the module that should own the column,")
430
+ lines.append(u"# for a reference that would otherwise invert a dependency.")
431
+ lines.append(u"EXTENSIONS = {}")
432
+ lines.append(u"")
433
+ lines.append(u"# Objects an authored spec covers. Nothing is authored here.")
434
+ lines.append(u"DONE = set()")
435
+ lines.append(u"")
436
+ lines.append(u"")
437
+ lines.append(u"def names(contour):")
438
+ lines.append(u'\t"""The 1C object names one contour owns."""')
439
+ lines.append(u'\treturn list(contour["objects"])')
440
+ lines.append(u"")
441
+
442
+ path = os.path.join(OUT, "contours_subsystems.py")
443
+ with io.open(path, "w", encoding="utf-8", newline="\n") as f:
444
+ f.write(u"\n".join(lines))
445
+ print("%s: %d module(s), %d object(s)"
446
+ % (path, len(assigned), sum(len(v) for v in assigned.values())))
447
+ for code in sorted(assigned, key=lambda c: -len(assigned[c])):
448
+ print(" %-34s %4d object(s)" % (code, len(assigned[code])))
449
+
450
+ def main():
451
+ args = sys.argv[1:]
452
+ found = collect()
453
+
454
+ if "--json" in args:
455
+ path = os.path.join(OUT, "subsystems.json")
456
+ with io.open(path, "w", encoding="utf-8") as f:
457
+ f.write(json.dumps(found, ensure_ascii=False, indent=1,
458
+ sort_keys=True))
459
+ print("%s: %d subsystem(s)" % (path, len(found)))
460
+ return
461
+ if "--overlap" in args:
462
+ return overlap(found)
463
+ if "--orphans" in args:
464
+ return orphans(found)
465
+ if "--contours" in args:
466
+ return emit_contours(found, acyclic="--acyclic" in args)
467
+ if "--name" in args:
468
+ return detail(found, args[args.index("--name") + 1])
469
+ if "--objects" in args:
470
+ for sub in found:
471
+ print("\n%s %s %d rows" % (sub["name"], sub["synonym"], sub["rows"]))
472
+ for held in sorted(sub["objects"], key=lambda h: -h["rows"])[:12]:
473
+ print(" %-16s %-44s %12d" % (held["family"], held["name"][:44],
474
+ held["rows"]))
475
+ return
476
+ table(found)
477
+
478
+
479
+ if __name__ == "__main__":
480
+ main()