@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,126 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
The infobase as everything past the export sees it: `out/data/<table>.parquet`, one view
|
|
4
|
+
per table, queried with DuckDB. Nothing here knows whether the export came from MS SQL or
|
|
5
|
+
from a file infobase - `dump1c` made them the same - and nothing past here talks to 1C.
|
|
6
|
+
|
|
7
|
+
cur = store.connect().cursor()
|
|
8
|
+
cur.execute('SELECT "_IDRRef", "_EnumOrder" FROM "_Enum12"')
|
|
9
|
+
|
|
10
|
+
The SQL is DuckDB's, so a project's `sources.QUERIES` are too: names in double quotes
|
|
11
|
+
(DuckDB matches them without regard to case), `LIMIT` for `TOP`, `from_hex('..')` - see
|
|
12
|
+
`literal` - for a binary constant. The values are as 1C stores them: a server infobase's
|
|
13
|
+
dates still carry its year offset (`year_offset()`), a boolean is binary(1).
|
|
14
|
+
|
|
15
|
+
The cursor takes parameters the way pyodbc did, `execute(sql, a, b)` or `execute(sql, [a, b])`,
|
|
16
|
+
so the readers of the infobase kept their shape when they stopped reading MS SQL.
|
|
17
|
+
"""
|
|
18
|
+
import codecs
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
import paths
|
|
23
|
+
|
|
24
|
+
DATA = os.path.join(paths.OUT, "data")
|
|
25
|
+
MANIFEST = os.path.join(paths.OUT, "export.json")
|
|
26
|
+
|
|
27
|
+
# A boolean as 1C stores it, binary(1): `_Posted`, `_Marked`, `_Folder`.
|
|
28
|
+
TRUE = b"\x01"
|
|
29
|
+
|
|
30
|
+
_MANIFEST = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def manifest():
|
|
34
|
+
if not _MANIFEST:
|
|
35
|
+
if not os.path.exists(MANIFEST):
|
|
36
|
+
raise SystemExit("no export in %s - run `1c.py dump1c all` first" % paths.OUT)
|
|
37
|
+
with codecs.open(MANIFEST, "r", "utf-8") as f:
|
|
38
|
+
_MANIFEST.append(json.load(f))
|
|
39
|
+
return _MANIFEST[0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def year_offset():
|
|
43
|
+
"""What 1C adds to a year when it stores a date: 2000 on MS SQL as a rule, 0 in a file."""
|
|
44
|
+
return int(manifest().get("year_offset", 0))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def source_kind():
|
|
48
|
+
return manifest()["source"]["kind"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def horizon():
|
|
52
|
+
"""The oldest horizon any exported table was read after, or None for a file export.
|
|
53
|
+
|
|
54
|
+
Every change 1C committed below it is in the export. One mark for the whole export
|
|
55
|
+
rather than one per table is the cautious reading: a declared query may join tables
|
|
56
|
+
read at different moments, and a mark too low only re-reads, never misses.
|
|
57
|
+
"""
|
|
58
|
+
marks = [t.get("horizon") for t in manifest()["tables"].values()]
|
|
59
|
+
if not marks or any(m is None for m in marks):
|
|
60
|
+
return None
|
|
61
|
+
return bytes.fromhex(min(marks))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def literal(raw):
|
|
65
|
+
"""A binary constant in SQL - what `0x...` was in T-SQL."""
|
|
66
|
+
return "from_hex('%s')" % bytes(raw).hex()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def quote(name):
|
|
70
|
+
return '"%s"' % name.replace('"', '""')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Cursor(object):
|
|
74
|
+
def __init__(self, con):
|
|
75
|
+
self.con = con
|
|
76
|
+
|
|
77
|
+
def execute(self, sql, *params):
|
|
78
|
+
if len(params) == 1 and isinstance(params[0], (list, tuple)):
|
|
79
|
+
params = params[0]
|
|
80
|
+
self.con.execute(sql, list(params))
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def description(self):
|
|
85
|
+
return self.con.description
|
|
86
|
+
|
|
87
|
+
def fetchone(self):
|
|
88
|
+
return self.con.fetchone()
|
|
89
|
+
|
|
90
|
+
def fetchall(self):
|
|
91
|
+
return self.con.fetchall()
|
|
92
|
+
|
|
93
|
+
def fetchmany(self, size):
|
|
94
|
+
return self.con.fetchmany(size)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Database(object):
|
|
98
|
+
def __init__(self):
|
|
99
|
+
import duckdb
|
|
100
|
+
self.con = duckdb.connect()
|
|
101
|
+
# Spill under out/, and only so far. A query that loses its window - a LATERAL join
|
|
102
|
+
# DuckDB filters above rather than at the scan - reads all 99.8 M analytic rows and
|
|
103
|
+
# spilled 17 GB before it died, nearly the whole of the disk it shared with the system.
|
|
104
|
+
spill = os.path.join(paths.OUT, ".duckdb")
|
|
105
|
+
self.con.execute("SET temp_directory = '%s'" % spill.replace("\\", "/").replace("'", "''"))
|
|
106
|
+
self.con.execute("SET max_temp_directory_size = '%s'" % os.environ.get("DF1C_DUCKDB_TEMP", "8GB"))
|
|
107
|
+
tables = manifest()["tables"]
|
|
108
|
+
for name in sorted(tables):
|
|
109
|
+
path = os.path.join(DATA, name + ".parquet")
|
|
110
|
+
if os.path.exists(path):
|
|
111
|
+
self.con.execute("CREATE VIEW %s AS SELECT * FROM read_parquet('%s')"
|
|
112
|
+
% (quote(name), path.replace("\\", "/").replace("'", "''")))
|
|
113
|
+
self.tables = tables
|
|
114
|
+
|
|
115
|
+
def cursor(self):
|
|
116
|
+
return Cursor(self.con.cursor())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_DB = []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def connect():
|
|
123
|
+
"""One DuckDB per process; each cursor is its own connection to it."""
|
|
124
|
+
if not _DB:
|
|
125
|
+
_DB.append(Database())
|
|
126
|
+
return _DB[0]
|
|
@@ -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()
|