@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,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()