@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.
- package/CHANGELOG.md +73 -0
- package/README.md +3 -2
- package/dist/server.js +109 -84
- package/package.json +48 -46
- package/resources/docs/conventions.md +17 -14
- package/resources/docs/dsl-reference.md +15 -5
- package/resources/schemas/entity.schema.json +1 -1
- package/resources/schemas/registers.schema.json +8 -3
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +374 -0
- package/skills/dforge-1c-import/references/environment.md +97 -0
- package/skills/dforge-1c-import/references/loading.md +161 -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 +110 -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/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 +741 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +201 -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 +315 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -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/paths.py +90 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +70 -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 +94 -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 +488 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -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/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/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
- package/skills/dforge-mcp-author/SKILL.md +8 -2
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
- package/skills/dforge-mcp-author/references/conventions.md +7 -1
- package/skills/dforge-mcp-author/references/field-types.md +3 -2
- package/skills/dforge-mcp-author/references/formulas.md +5 -1
- package/skills/dforge-mcp-author/references/print-templates.md +5 -2
- package/skills/dforge-mcp-author/references/queries.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +18 -3
- package/skills/dforge-mcp-author/references/reports.md +4 -1
- package/skills/dforge-mcp-author/references/schema-import.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +1 -1
- package/skills/dforge-mcp-author/references/translations.md +38 -15
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
- package/skills/dforge-module-build/SKILL.md +10 -1
- package/skills/dforge-module-ship/SKILL.md +31 -3
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""What each document's posting handler actually writes.
|
|
4
|
+
|
|
5
|
+
The first measurement of this counted the body of `ОбработкаПроведения` and stopped there,
|
|
6
|
+
and that is not what a handler writes. A four-line handler that calls a procedure writing
|
|
7
|
+
two registers per line is not a four-line handler, and a document that writes movements
|
|
8
|
+
from `ПриЗаписи` does not write them only when posted. Both shapes are in this base, and
|
|
9
|
+
between them they moved the "ports one for one" list by more than a name.
|
|
10
|
+
|
|
11
|
+
So this follows the calls. For every document module that has a posting handler:
|
|
12
|
+
|
|
13
|
+
* the registers the handler names directly;
|
|
14
|
+
* the registers named by any procedure in the same module that the handler calls;
|
|
15
|
+
* the registers written outside posting altogether, from `ПриЗаписи` / `ПередЗаписью`.
|
|
16
|
+
|
|
17
|
+
A name only counts when the metadata has a register by it. `Движения.` also reaches
|
|
18
|
+
accounting-movement collections and a few properties, and counting those inflated an
|
|
19
|
+
earlier figure by half.
|
|
20
|
+
|
|
21
|
+
python tools/1c.py handlers # the summary, and out/handlers.json
|
|
22
|
+
python tools/1c.py handlers --client # the client's own documents, classified
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import print_function
|
|
25
|
+
|
|
26
|
+
import codecs
|
|
27
|
+
import io
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
import binding
|
|
34
|
+
import paths
|
|
35
|
+
|
|
36
|
+
CODE = os.path.join(paths.OUT, "code", "Document")
|
|
37
|
+
|
|
38
|
+
POST = u"ОбработкаПроведения"
|
|
39
|
+
ON_WRITE = u"ПриЗаписи"
|
|
40
|
+
BEFORE_WRITE = u"ПередЗаписью"
|
|
41
|
+
|
|
42
|
+
# The client's own development carries the project's prefixes; everything else is the
|
|
43
|
+
# vendor's configuration, whose registers the port deliberately does not carry.
|
|
44
|
+
CLIENT = binding.client_prefixes()
|
|
45
|
+
|
|
46
|
+
WORD = u"[А-Яа-яёЁA-Za-z_][А-Яа-яёЁA-Za-z_0-9]*"
|
|
47
|
+
PROC = re.compile(u"^[ \t]*(?:Процедура|Функция)[ \t]+(%s)" % WORD, re.M | re.U)
|
|
48
|
+
END = re.compile(u"^[ \t]*(?:КонецПроцедуры|КонецФункции)", re.M | re.U)
|
|
49
|
+
REGISTER = re.compile(u"Движения\\.(%s)" % WORD, re.U)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def procedures(text):
|
|
53
|
+
"""Procedure name -> its source. 1C does not nest them, so this is a flat scan."""
|
|
54
|
+
found = {}
|
|
55
|
+
lines = text.splitlines()
|
|
56
|
+
index = 0
|
|
57
|
+
while index < len(lines):
|
|
58
|
+
match = PROC.match(lines[index])
|
|
59
|
+
if not match:
|
|
60
|
+
index += 1
|
|
61
|
+
continue
|
|
62
|
+
end = index + 1
|
|
63
|
+
while end < len(lines) and not END.match(lines[end]):
|
|
64
|
+
end += 1
|
|
65
|
+
found[match.group(1)] = "\n".join(lines[index:end + 1])
|
|
66
|
+
index = end + 1
|
|
67
|
+
return found
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def calls(body, name):
|
|
71
|
+
"""Does `body` call `name`? A bare mention in a comment or a longer name is not one."""
|
|
72
|
+
return bool(re.search(u"(?<![А-Яа-яёЁA-Za-z_0-9])%s[ \t]*\\(" % re.escape(name),
|
|
73
|
+
body, re.U))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def scan():
|
|
77
|
+
"""One row per document that has a posting handler."""
|
|
78
|
+
rows = []
|
|
79
|
+
for document in sorted(os.listdir(CODE)):
|
|
80
|
+
directory = os.path.join(CODE, document)
|
|
81
|
+
if not os.path.isdir(directory):
|
|
82
|
+
continue
|
|
83
|
+
for name in sorted(os.listdir(directory)):
|
|
84
|
+
if not name.endswith(".bsl"):
|
|
85
|
+
continue
|
|
86
|
+
with io.open(os.path.join(directory, name), encoding="utf-8",
|
|
87
|
+
errors="replace") as handle:
|
|
88
|
+
text = handle.read()
|
|
89
|
+
if POST not in text:
|
|
90
|
+
continue
|
|
91
|
+
bodies = procedures(text)
|
|
92
|
+
if POST not in bodies:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
direct = set(REGISTER.findall(bodies[POST]))
|
|
96
|
+
via = {}
|
|
97
|
+
for procedure, body in bodies.items():
|
|
98
|
+
if procedure == POST or not REGISTER.search(body):
|
|
99
|
+
continue
|
|
100
|
+
if calls(bodies[POST], procedure):
|
|
101
|
+
via[procedure] = sorted(set(REGISTER.findall(body)))
|
|
102
|
+
|
|
103
|
+
outside = {}
|
|
104
|
+
for hook in (ON_WRITE, BEFORE_WRITE):
|
|
105
|
+
if hook in bodies and REGISTER.search(bodies[hook]):
|
|
106
|
+
outside[hook] = sorted(set(REGISTER.findall(bodies[hook])))
|
|
107
|
+
|
|
108
|
+
rows.append({
|
|
109
|
+
"doc": document,
|
|
110
|
+
"module": name,
|
|
111
|
+
"postLines": len(bodies[POST].splitlines()),
|
|
112
|
+
"direct": sorted(direct),
|
|
113
|
+
"viaCalls": via,
|
|
114
|
+
"outsidePosting": outside,
|
|
115
|
+
})
|
|
116
|
+
break
|
|
117
|
+
return rows
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def declared_registers():
|
|
121
|
+
with codecs.open(os.path.join(paths.OUT, "objects_all.json"), "r", "utf-8") as handle:
|
|
122
|
+
objects = json.load(handle)
|
|
123
|
+
return set(o["name"] for o in objects
|
|
124
|
+
if o.get("family") in ("InfoRg", "AccumRg", "AccRg") and o.get("name"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def classify(rows, registers):
|
|
128
|
+
"""The client's documents, by whose registers they write."""
|
|
129
|
+
out = []
|
|
130
|
+
for row in rows:
|
|
131
|
+
if not row["doc"].startswith(CLIENT):
|
|
132
|
+
continue
|
|
133
|
+
direct = set(row["direct"])
|
|
134
|
+
via = set()
|
|
135
|
+
for names in row["viaCalls"].values():
|
|
136
|
+
via |= set(names)
|
|
137
|
+
outside = set()
|
|
138
|
+
for names in row["outsidePosting"].values():
|
|
139
|
+
outside |= set(names)
|
|
140
|
+
|
|
141
|
+
every = (direct | via | outside) & registers
|
|
142
|
+
if not every:
|
|
143
|
+
continue
|
|
144
|
+
out.append({
|
|
145
|
+
"doc": row["doc"],
|
|
146
|
+
"postLines": row["postLines"],
|
|
147
|
+
"client": sorted(n for n in every if n.startswith(CLIENT)),
|
|
148
|
+
"vendor": sorted(n for n in every if not n.startswith(CLIENT)),
|
|
149
|
+
"viaCalls": sorted((via & registers) - direct),
|
|
150
|
+
"outsidePosting": sorted(outside & registers),
|
|
151
|
+
})
|
|
152
|
+
return out
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def write_json(name, payload):
|
|
156
|
+
path = os.path.join(paths.OUT, name)
|
|
157
|
+
with codecs.open(path, "w", "utf-8") as handle:
|
|
158
|
+
handle.write(json.dumps(payload, ensure_ascii=False, indent="\t", sort_keys=True))
|
|
159
|
+
return path
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
rows = scan()
|
|
164
|
+
registers = declared_registers()
|
|
165
|
+
client = classify(rows, registers)
|
|
166
|
+
|
|
167
|
+
write_json("handlers.json", rows)
|
|
168
|
+
write_json("handlers-client.json", client)
|
|
169
|
+
|
|
170
|
+
print("registers the metadata declares: %d" % len(registers))
|
|
171
|
+
print("documents with a posting handler: %d" % len(rows))
|
|
172
|
+
print(" whose handler calls a procedure writing a register: %d"
|
|
173
|
+
% len([r for r in rows if r["viaCalls"]]))
|
|
174
|
+
print(" writing movements outside posting as well: %d"
|
|
175
|
+
% len([r for r in rows if r["outsidePosting"]]))
|
|
176
|
+
|
|
177
|
+
pure = [r for r in client if not r["vendor"]]
|
|
178
|
+
mixed = [r for r in client if r["vendor"] and r["client"]]
|
|
179
|
+
vendor = [r for r in client if r["vendor"] and not r["client"]]
|
|
180
|
+
print()
|
|
181
|
+
print("the client's documents that write a register: %d" % len(client))
|
|
182
|
+
print(" only the client's own registers: %d" % len(pure))
|
|
183
|
+
print(" mixed: %d" % len(mixed))
|
|
184
|
+
print(" only the vendor's: %d" % len(vendor))
|
|
185
|
+
|
|
186
|
+
if "--client" not in sys.argv:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
print()
|
|
190
|
+
print("Ports one for one, shortest first:")
|
|
191
|
+
for row in sorted(pure, key=lambda r: r["postLines"]):
|
|
192
|
+
print(" %-54s %3d %s"
|
|
193
|
+
% (row["doc"], row["postLines"], ", ".join(row["client"])))
|
|
194
|
+
if row["viaCalls"]:
|
|
195
|
+
print(" %-54s via calls: %s" % ("", ", ".join(row["viaCalls"])))
|
|
196
|
+
if row["outsidePosting"]:
|
|
197
|
+
print(" %-54s OUTSIDE POSTING: %s"
|
|
198
|
+
% ("", ", ".join(row["outsidePosting"])))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Propose the 1C source of every column in a hand-written spec.
|
|
5
|
+
|
|
6
|
+
`fleet`, `shipping` and `masterdata` were authored, so their column names were chosen
|
|
7
|
+
rather than generated, and nothing in the package records which 1C attribute each one
|
|
8
|
+
came from. The migration needs exactly that.
|
|
9
|
+
|
|
10
|
+
Most of it can be proposed rather than typed: the names were chosen *from* the 1C
|
|
11
|
+
attributes, so running the attribute through `naming.code` lands on the same word most of
|
|
12
|
+
the time - `БазоваяЕдиницаИзмерения` gives `base_uom_measure` against a column called
|
|
13
|
+
`base_uom`. This scores those candidates and prints what it is sure of, what it guessed
|
|
14
|
+
and what it could not place, so the confirmed result can go into `sources.py` and the
|
|
15
|
+
remainder can be decided by eye.
|
|
16
|
+
|
|
17
|
+
python tools/handmap.py # every hand-written module
|
|
18
|
+
python tools/handmap.py masterdata # one of them
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import print_function
|
|
21
|
+
|
|
22
|
+
import collections
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import autospec
|
|
26
|
+
import naming
|
|
27
|
+
import binding
|
|
28
|
+
|
|
29
|
+
sources = binding.sources()
|
|
30
|
+
|
|
31
|
+
# The authored modules, whichever the project happens to have.
|
|
32
|
+
SPECS = binding.specs()
|
|
33
|
+
|
|
34
|
+
# Columns the 1C object carries in its own right rather than as an attribute; these are
|
|
35
|
+
# resolved from the physical table, exactly as `autospec` resolves them.
|
|
36
|
+
STANDARD = set(autospec.STANDARD_PHYSICAL)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def tokens(code):
|
|
40
|
+
return set(code.split("_"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def score(column, candidate):
|
|
44
|
+
"""How well a 1C attribute's generated code matches a hand-chosen column name."""
|
|
45
|
+
if column == candidate:
|
|
46
|
+
return 100
|
|
47
|
+
ours, theirs = tokens(column), tokens(candidate)
|
|
48
|
+
if not ours or not theirs:
|
|
49
|
+
return 0
|
|
50
|
+
shared = ours & theirs
|
|
51
|
+
if not shared:
|
|
52
|
+
return 0
|
|
53
|
+
# Every word of the shorter name present in the longer one is a strong signal:
|
|
54
|
+
# `base_uom` against `base_uom_measure`.
|
|
55
|
+
if ours <= theirs or theirs <= ours:
|
|
56
|
+
return 90 - abs(len(ours) - len(theirs))
|
|
57
|
+
return int(60 * len(shared) / float(len(ours | theirs)))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def propose(module, entity, spec, model):
|
|
61
|
+
"""(confirmed, guessed, unplaced) for one entity."""
|
|
62
|
+
declared = sources.SOURCES.get(module, {}).get(entity, {})
|
|
63
|
+
object_name = declared.get("object")
|
|
64
|
+
if not object_name:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
family = declared.get("family")
|
|
68
|
+
section = declared.get("section")
|
|
69
|
+
fields = [f for f in model.fields(object_name, family)
|
|
70
|
+
if (f.get("section") or None) == section and not naming.is_dead(f["name"])]
|
|
71
|
+
candidates = [(naming.code(f["name"], 48), f) for f in fields]
|
|
72
|
+
|
|
73
|
+
manual = declared.get("columns", {})
|
|
74
|
+
confirmed, guessed, unplaced = [], [], []
|
|
75
|
+
taken = set(manual.values())
|
|
76
|
+
|
|
77
|
+
for item in spec["fields"]:
|
|
78
|
+
column = item[0]
|
|
79
|
+
if column in manual:
|
|
80
|
+
confirmed.append((column, manual[column], "declared"))
|
|
81
|
+
continue
|
|
82
|
+
if column in STANDARD:
|
|
83
|
+
confirmed.append((column, "(standard)", "standard"))
|
|
84
|
+
continue
|
|
85
|
+
# Sorted on the score and the code only. Two candidates scoring the same
|
|
86
|
+
# under the same code made the tuple comparison reach the field dicts, which
|
|
87
|
+
# do not compare - a latent defect until an object turned up whose attributes
|
|
88
|
+
# generate the same code twice.
|
|
89
|
+
ranked = sorted(
|
|
90
|
+
((score(column, code), code, field) for code, field in candidates
|
|
91
|
+
if field["name"] not in taken),
|
|
92
|
+
key=lambda ranked_row: (ranked_row[0], ranked_row[1]), reverse=True)
|
|
93
|
+
if ranked and ranked[0][0] >= 90:
|
|
94
|
+
confirmed.append((column, ranked[0][2]["name"], "matched"))
|
|
95
|
+
taken.add(ranked[0][2]["name"])
|
|
96
|
+
elif ranked and ranked[0][0] >= 40:
|
|
97
|
+
guessed.append((column, ranked[0][2]["name"], ranked[0][0]))
|
|
98
|
+
else:
|
|
99
|
+
unplaced.append(column)
|
|
100
|
+
|
|
101
|
+
spare = [f["name"] for _, f in candidates if f["name"] not in taken]
|
|
102
|
+
return confirmed, guessed, unplaced, spare, object_name
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
model = autospec.Model()
|
|
107
|
+
wanted = sys.argv[1] if len(sys.argv) > 1 else None
|
|
108
|
+
totals = collections.Counter()
|
|
109
|
+
|
|
110
|
+
for module, spec in SPECS.items():
|
|
111
|
+
if wanted and module != wanted:
|
|
112
|
+
continue
|
|
113
|
+
print("\n=== %s ===" % module)
|
|
114
|
+
for entity, entity_spec in spec["entities"].items():
|
|
115
|
+
found = propose(module, entity, entity_spec, model)
|
|
116
|
+
if found is None:
|
|
117
|
+
print("\n%-28s no 1C object declared in sources.py" % entity)
|
|
118
|
+
totals["undeclared"] += len(entity_spec["fields"])
|
|
119
|
+
continue
|
|
120
|
+
confirmed, guessed, unplaced, spare, object_name = found
|
|
121
|
+
totals["confirmed"] += len(confirmed)
|
|
122
|
+
totals["guessed"] += len(guessed)
|
|
123
|
+
totals["unplaced"] += len(unplaced)
|
|
124
|
+
print("\n%-28s <- %s (%d confirmed, %d to check, %d unplaced)"
|
|
125
|
+
% (entity, object_name, len(confirmed), len(guessed), len(unplaced)))
|
|
126
|
+
for column, name, how in confirmed:
|
|
127
|
+
if how == "matched":
|
|
128
|
+
print(" ok %-34s %s" % (column, name))
|
|
129
|
+
for column, name, points in guessed:
|
|
130
|
+
print(" ?%-3d %-34s %s" % (points, column, name))
|
|
131
|
+
for column in unplaced:
|
|
132
|
+
print(" -- %-34s" % column)
|
|
133
|
+
if spare:
|
|
134
|
+
print(" 1C attributes nothing claimed: %s" % ", ".join(spare[:8]))
|
|
135
|
+
|
|
136
|
+
print("\n%d confirmed, %d to check by eye, %d unplaced, %d in undeclared entities"
|
|
137
|
+
% (totals["confirmed"], totals["guessed"], totals["unplaced"],
|
|
138
|
+
totals["undeclared"]))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
How much of the configuration is actually translated.
|
|
5
|
+
|
|
6
|
+
1C synonyms are per-language: {2,"ru","Водители","uk","Водії"}. Whether a given object
|
|
7
|
+
or field carries a `uk` synonym at all decides whether the port can lift its Ukrainian
|
|
8
|
+
labels from the infobase or has to translate them itself.
|
|
9
|
+
|
|
10
|
+
python tools/i18n_coverage.py # whole configuration, by family
|
|
11
|
+
python tools/i18n_coverage.py <names...> # named objects, per field
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import print_function
|
|
14
|
+
|
|
15
|
+
import collections
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import attrs
|
|
19
|
+
import meta
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def synonyms(node):
|
|
23
|
+
"""Identity block -> {lang: text}. The block is [count, lang, text, lang, text, ...]."""
|
|
24
|
+
block = node[1][3] if len(node[1]) > 3 else None
|
|
25
|
+
if not isinstance(block, list) or len(block) < 3:
|
|
26
|
+
return {}
|
|
27
|
+
body = block[1:]
|
|
28
|
+
return {body[i]: body[i + 1] for i in range(0, len(body) - 1, 2)
|
|
29
|
+
if isinstance(body[i], str) and len(body[i]) == 2}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def object_languages(guid, name):
|
|
33
|
+
"""(object synonyms, [(field, {lang: text}), ...])"""
|
|
34
|
+
text = meta.load(guid)
|
|
35
|
+
if text is None:
|
|
36
|
+
return {}, []
|
|
37
|
+
tree = meta.parse(text)
|
|
38
|
+
own = {}
|
|
39
|
+
for node in meta.walk(tree):
|
|
40
|
+
if attrs.is_identity(node) and node[2] == name:
|
|
41
|
+
own = synonyms(["", node])
|
|
42
|
+
break
|
|
43
|
+
fields = []
|
|
44
|
+
for node in meta.walk(tree):
|
|
45
|
+
if attrs.is_field(node):
|
|
46
|
+
fields.append((attrs.name_of(node), synonyms(node)))
|
|
47
|
+
return own, fields
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
objects = attrs.load_json("objects.json")
|
|
52
|
+
report = {r["name"]: r for r in attrs.load_json("report.json")}
|
|
53
|
+
names = sys.argv[1:]
|
|
54
|
+
|
|
55
|
+
if names:
|
|
56
|
+
for name in names:
|
|
57
|
+
guid = next((o["guid"] for o in objects if o.get("name") == name), None)
|
|
58
|
+
own, fields = object_languages(guid, name)
|
|
59
|
+
print("\n== %s %s" % (name, " ".join("%s=%s" % kv for kv in sorted(own.items()))))
|
|
60
|
+
for field, langs in fields:
|
|
61
|
+
print(" %-34s %s" % (field, " | ".join(
|
|
62
|
+
"%s: %s" % (lang, langs[lang]) for lang in sorted(langs))))
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
stats = collections.defaultdict(lambda: {"objects": 0, "obj_uk": 0,
|
|
66
|
+
"fields": 0, "fld_uk": 0})
|
|
67
|
+
for obj in objects:
|
|
68
|
+
name = obj.get("name")
|
|
69
|
+
if not name:
|
|
70
|
+
continue
|
|
71
|
+
family = report.get(name, {}).get("family") or obj["family"]
|
|
72
|
+
own, fields = object_languages(obj["guid"], name)
|
|
73
|
+
row = stats[family]
|
|
74
|
+
row["objects"] += 1
|
|
75
|
+
row["obj_uk"] += 1 if "uk" in own else 0
|
|
76
|
+
row["fields"] += len(fields)
|
|
77
|
+
row["fld_uk"] += sum(1 for _, langs in fields if "uk" in langs)
|
|
78
|
+
|
|
79
|
+
print("%-18s %8s %8s %10s %8s" % ("family", "objects", "obj uk", "fields", "fld uk"))
|
|
80
|
+
print("-" * 56)
|
|
81
|
+
for family, row in sorted(stats.items(), key=lambda kv: -kv[1]["fields"]):
|
|
82
|
+
print("%-18s %8d %8d %10d %8d"
|
|
83
|
+
% (family, row["objects"], row["obj_uk"], row["fields"], row["fld_uk"]))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|