@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,94 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Which document types write a register, and how much.
5
+
6
+ A register with _RecorderTRef is written by several document kinds; the TRef is the
7
+ target's type id, which appears in the reference columns of any table pointing at the
8
+ same type. Matching it back to a document is done through DBNames: the type id is the
9
+ low bytes of the table number, so comparing against every document's table number
10
+ identifies it.
11
+
12
+ python tools/recorders.py _AccumRg17050
13
+ """
14
+ from __future__ import print_function
15
+
16
+ import codecs
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ import paths
22
+
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+
25
+ import dump1c
26
+
27
+ OUT = paths.OUT
28
+
29
+
30
+ def load_report():
31
+ with codecs.open(os.path.join(OUT, "report.json"), "r", "utf-8") as f:
32
+ return json.load(f)
33
+
34
+
35
+ def probe_single(cur, table):
36
+ """Without a TRef the recorder's type is implicit, so find the one document table
37
+ whose PK matches the recorders we sampled."""
38
+ cur.execute("SELECT DISTINCT TOP (20) _RecorderRRef FROM [%s]" % table)
39
+ refs = [bytes(r[0]) for r in cur.fetchall()]
40
+ if not refs:
41
+ print(" (register is empty)")
42
+ return
43
+ placeholders = ",".join("?" * len(refs))
44
+ for row in load_report():
45
+ physical = row.get("physical") or ""
46
+ suffix = physical[len("_Document"):] if physical.startswith("_Document") else ""
47
+ if not suffix.isdigit() or not row.get("rows"):
48
+ continue
49
+ cur.execute("SELECT COUNT(*) FROM [%s] WHERE _IDRRef IN (%s)"
50
+ % (physical, placeholders), *refs)
51
+ hits = cur.fetchone()[0]
52
+ if hits:
53
+ print(" %s - %s (%d/%d sampled recorders matched)"
54
+ % (row["name"], row["synonym"] or "", hits, len(refs)))
55
+ return
56
+ print(" no document table matched the sampled recorders")
57
+
58
+
59
+ def main():
60
+ table = sys.argv[1]
61
+ cur = dump1c.connect(paths.required("DF1C_SERVER", "which MS SQL server holds it"),
62
+ paths.required("DF1C_DB",
63
+ "which MS SQL infobase to read")).cursor()
64
+ cur.execute("""SELECT COUNT(*) FROM sys.columns
65
+ WHERE object_id = OBJECT_ID(?) AND name = '_RecorderTRef'""", table)
66
+ if not cur.fetchone()[0]:
67
+ print("%s has no _RecorderTRef - one document type writes it; probing for it"
68
+ % table)
69
+ probe_single(cur, table)
70
+ return
71
+
72
+ cur.execute("SELECT _RecorderTRef, COUNT(*) FROM [%s] GROUP BY _RecorderTRef "
73
+ "ORDER BY 2 DESC" % table)
74
+ rows = cur.fetchall()
75
+
76
+ # a document's type id is its table number; _RecorderTRef stores it big-endian
77
+ by_number = {}
78
+ for row in load_report():
79
+ physical = row.get("physical") or ""
80
+ suffix = physical[len("_Document"):] if physical.startswith("_Document") else ""
81
+ if suffix.isdigit(): # skips _DocumentJournal<N>
82
+ by_number[int(suffix)] = row
83
+
84
+ print("%-12s %-12s %s" % ("TRef", "movements", "document"))
85
+ for tref, n in rows:
86
+ number = int.from_bytes(bytes(tref), "big") & 0xFFFF
87
+ doc = by_number.get(number)
88
+ print("%-12s %-12s %s" % (bytes(tref).hex(), "{:,}".format(n),
89
+ "%s - %s" % (doc["name"], doc["synonym"] or "") if doc
90
+ else "number=%d (unmatched)" % number))
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Does every authored reference point where the 1C attribute actually points?
5
+
6
+ The generated modules resolve a reference from the attribute's declared type, so they
7
+ cannot get this wrong. The authored ones name their target by hand, and a name is a poor
8
+ guide: `БазоваяЕдиницаИзмерения` reads like the product's own units and points at the unit
9
+ classifier, and `МестоОтгрузки` reads like a counterparty and is not one.
10
+
11
+ A wrong target costs nothing until the data arrives, and then it is every row: 7,981 of
12
+ 8,400 products dangled on the first load of `masterdata`.
13
+
14
+ python tools/reftargets.py
15
+ """
16
+ from __future__ import print_function
17
+
18
+ import codecs
19
+ import json
20
+ import os
21
+ import sys
22
+
23
+ import attrs
24
+ import autospec
25
+ import handmap
26
+ import binding
27
+
28
+ sources = binding.sources()
29
+
30
+
31
+ def ported(mapping):
32
+ """1C object name -> the entity the port gave it."""
33
+ out = {}
34
+ for module, entities in mapping.items():
35
+ for entity, found in entities.items():
36
+ name = (found.get("object") or "").split(" / ")[0]
37
+ if name and "columns" in found:
38
+ out.setdefault(name, "%s.%s" % (module, entity))
39
+ return out
40
+
41
+
42
+ def main():
43
+ model = autospec.Model()
44
+ path = os.path.join(attrs.OUT, "columnmap.json")
45
+ with codecs.open(path, "r", "utf-8") as f:
46
+ mapping = json.load(f)
47
+ owner = ported(mapping)
48
+
49
+ wrong = 0
50
+ for module in sorted(handmap.SPECS):
51
+ for entity, spec in handmap.SPECS[module]["entities"].items():
52
+ declared = sources.SOURCES.get(module, {}).get(entity, {})
53
+ name = declared.get("object")
54
+ if not name:
55
+ continue
56
+ family, section = declared.get("family"), declared.get("section")
57
+ _, columns = model.physical(name, family)
58
+ by_physical = {}
59
+ for field in model.fields(name, family):
60
+ if (field.get("section") or None) != section:
61
+ continue
62
+ physical = autospec.physical_of(field, model, columns)
63
+ if physical:
64
+ by_physical[physical] = field
65
+
66
+ for item in spec["fields"]:
67
+ if not item[1].startswith("ref:"):
68
+ continue
69
+ target = item[1][len("ref:"):]
70
+ if "." not in target:
71
+ target = module + "." + target
72
+ physical = mapping[module][entity]["columns"].get(item[0] + "_id")
73
+ field = by_physical.get(physical)
74
+ if not field:
75
+ continue
76
+ declared_targets = [p.strip()[3:].strip()
77
+ for p in (field["type"] or "").split(",")
78
+ if p.strip().startswith("->")]
79
+ real = [t.split(".", 1)[1] for t in declared_targets if "." in t]
80
+ if not real:
81
+ continue
82
+ expected = [owner.get(r) for r in real]
83
+ if target in expected:
84
+ continue
85
+ wrong += 1
86
+ print("%-10s %-22s %-20s declared %-38s 1C: %s -> %s"
87
+ % (module, entity, item[0], target, ", ".join(real),
88
+ ", ".join(str(e) for e in expected)))
89
+
90
+ print("\n%d reference(s) pointing at the wrong entity" % wrong)
91
+ return 1 if wrong else 0
92
+
93
+
94
+ if __name__ == "__main__":
95
+ sys.exit(main())
@@ -0,0 +1,157 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Write a module's ported 1C reports from one compact spec.
3
+
4
+ A report over a stored procedure is declared in four places that have to agree - the
5
+ report (`ui/reports.json`), the procedure's call signature and columns
6
+ (`logic/stored_procedures.json`), the Ukrainian labels of both (`translations/uk-UA.json`)
7
+ and the rights to run them (`security/roles.json`) - and the platform binds them by code,
8
+ so a parameter spelled one way in the report and another in the procedure binds NULL
9
+ without a word. This writes all four from one spec, into the project's `authored/<module>/`
10
+ tree, which `genmodule` lays over the generated module; the menu gets a Reports section
11
+ from the reports file (genmodule.auto_menu).
12
+
13
+ A spec module (tools/project/reports_<module>.py) defines MODULE and REPORTS, an ordered
14
+ list of `report(...)`. The SQL of each procedure is the project's own file under
15
+ `authored/<module>/logic/reports/`.
16
+
17
+ python tools/lib/reportgen.py tools/project/reports_production.py
18
+ """
19
+ from __future__ import print_function
20
+
21
+ import codecs
22
+ import collections
23
+ import imp
24
+ import json
25
+ import os
26
+ import sys
27
+
28
+ O = collections.OrderedDict
29
+
30
+ KINDS = {
31
+ "money": ("currency", "number", 140),
32
+ "qty": ("number", "number", 120),
33
+ "int": ("number", "number", 70),
34
+ "date": ("date", "date", 110),
35
+ "datetime": ("datetime", "timestamp", 150),
36
+ "text": ("text", "string", 160),
37
+ "name": ("text", "string", 380),
38
+ "bool": ("checkbox", "bool", 80),
39
+ }
40
+
41
+ PG_FIELD = {"date": "date", "integer": "number", "numeric": "number", "boolean": "checkbox",
42
+ "text": "text", "uuid": "lookup"}
43
+
44
+
45
+ def col(code, en, uk, kind="text", width=None):
46
+ """A result column: its code, English and Ukrainian labels, and kind (KINDS)."""
47
+ return dict(code=code, en=en, uk=uk, kind=kind, width=width)
48
+
49
+
50
+ def param(code, pg, en, uk, required=False, default=None, link=None):
51
+ """A parameter: the procedure's argument and the report's, the same code in both. `link`
52
+ names the entity a uuid parameter picks from (`masterdata.warehouse`)."""
53
+ return dict(code=code, pg=pg, en=en, uk=uk, required=required, default=default, link=link)
54
+
55
+
56
+ def report(code, en, uk, sp, description, params, columns, source=None):
57
+ """A report over one procedure. `source` names the 1C report it ports."""
58
+ return dict(code=code, en=en, uk=uk, sp=sp, description=description, params=params,
59
+ columns=columns, source=source)
60
+
61
+
62
+ def date_from(required=True):
63
+ return param("p_date_from", "date", "From", u"Початок періоду", required, "=STARTMONTH(NOW())")
64
+
65
+
66
+ def date_to(required=True):
67
+ return param("p_date_to", "date", "To", u"Кінець періоду", required, "=ENDMONTH(NOW())")
68
+
69
+
70
+ def _field(p):
71
+ field = O([("fieldTypeCd", "lookup" if p["link"] else PG_FIELD.get(p["pg"], "text")),
72
+ ("label", p["en"]), ("required", p["required"])])
73
+ if p["default"] is not None:
74
+ field["default"] = p["default"]
75
+ if p["link"]:
76
+ field["params"] = {"link": {"entity": p["link"]}}
77
+ return field
78
+
79
+
80
+ def _read(path):
81
+ if not os.path.isfile(path):
82
+ return O()
83
+ with codecs.open(path, "r", "utf-8") as f:
84
+ return json.load(f, object_pairs_hook=O)
85
+
86
+
87
+ def _write(path, doc):
88
+ directory = os.path.dirname(path)
89
+ if not os.path.isdir(directory):
90
+ os.makedirs(directory)
91
+ with codecs.open(path, "w", "utf-8") as f:
92
+ f.write(json.dumps(doc, ensure_ascii=False, indent="\t") + "\n")
93
+
94
+
95
+ def build(module, reports, roles, root):
96
+ """Write the four files for `module` under `root` (authored/<module>/)."""
97
+ ui, sps, uk_reports, uk_sps, rights = O(), O(), O(), O(), O()
98
+ for r in reports:
99
+ columns = []
100
+ for c in r["columns"]:
101
+ field, base, width = KINDS[c["kind"]]
102
+ columns.append(O([("columnCd", c["code"]), ("label", c["en"]), ("fieldTypeCd", field),
103
+ ("baseDatatypeCd", base), ("width", c["width"] or width)]))
104
+ sps[r["sp"]] = O([
105
+ ("description", r["description"]), ("functionName", r["sp"]),
106
+ ("params", [O([("paramCd", p["code"]), ("pgType", p["pg"]),
107
+ ("fieldTypeCd", _field(p)["fieldTypeCd"]), ("label", p["en"]),
108
+ ("required", p["required"])]) for p in r["params"]]),
109
+ ("columns", columns)])
110
+ uk_sps[r["sp"]] = {"columns": O((c["code"], {"label": c["uk"]}) for c in r["columns"])}
111
+ ui[r["code"]] = O([
112
+ ("label", r["en"]),
113
+ ("description", r["description"] + (" (1C %s)" % r["source"] if r["source"] else "")),
114
+ ("parameters", O((p["code"], _field(p)) for p in r["params"])),
115
+ ("layout", {"panels": [O([("vizType", "table"), ("datasetCd", "rows"), ("title", r["en"])])]}),
116
+ ("datasets", {"rows": O([("caption", r["en"]), ("datasetType", "S"), ("spCd", r["sp"])])})])
117
+ uk_reports[r["code"]] = O([("label", r["uk"]),
118
+ ("params", O((p["code"], {"label": p["uk"]}) for p in r["params"])),
119
+ ("datasets", {"rows": {"caption": r["uk"]}})])
120
+ rights["report:" + r["code"]] = "E"
121
+ rights["sp:" + r["sp"]] = "E"
122
+
123
+ _write(os.path.join(root, "ui", "reports.json"), ui)
124
+ # the print forms' procedures (printgen, prn_*) share the file and are kept
125
+ kept = _read(os.path.join(root, "logic", "stored_procedures.json"))
126
+ sps.update((k, v) for k, v in kept.items() if k.startswith("prn_"))
127
+ _write(os.path.join(root, "logic", "stored_procedures.json"), sps)
128
+
129
+ path = os.path.join(root, "translations", "uk-UA.json")
130
+ translations = _read(path)
131
+ translations["reports"] = uk_reports
132
+ translations["storedProcedures"] = uk_sps
133
+ _write(path, translations)
134
+
135
+ path = os.path.join(root, "security", "roles.json")
136
+ doc = _read(path)
137
+ for role in roles:
138
+ entry = doc.setdefault(role, O([("rights", O())]))
139
+ entry.setdefault("rights", O())
140
+ for key in [k for k in entry["rights"] if k.startswith(("report:", "sp:")) and not k.startswith("sp:prn_")]:
141
+ del entry["rights"][key]
142
+ entry["rights"].update(rights)
143
+ _write(path, doc)
144
+ return len(reports)
145
+
146
+
147
+ def main(argv):
148
+ here = os.path.dirname(os.path.abspath(__file__))
149
+ for spec_path in argv[1:]:
150
+ spec = imp.load_source(os.path.splitext(os.path.basename(spec_path))[0], spec_path)
151
+ root = os.path.join(here, "..", "project", "authored", spec.MODULE)
152
+ count = build(spec.MODULE, spec.REPORTS, spec.ROLES, os.path.normpath(root))
153
+ print("%s: %d report(s)" % (spec.MODULE, count))
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main(sys.argv)
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """List the distinct collection class GUIDs across a set of objects, with samples,
4
+ so each one can be labelled (dimension / resource / attribute / tabular column)."""
5
+ from __future__ import print_function
6
+
7
+ import collections
8
+ import sys
9
+
10
+ import attrs
11
+
12
+ if __name__ == "__main__":
13
+ objects = attrs.load_json("objects.json")
14
+ types = attrs.load_json("typemap.json")
15
+ report = attrs.load_json("report.json")
16
+
17
+ names = sys.argv[1:] or [r["name"] for r in report if r.get("name")]
18
+ seen = collections.OrderedDict()
19
+ for name in names:
20
+ guid, fields = attrs.fields_of(name, objects, types)
21
+ if not fields:
22
+ continue
23
+ family = next((r["family"] for r in report if r["name"] == name), "?")
24
+ for f in fields:
25
+ key = (family, f["collection"])
26
+ entry = seen.setdefault(key, {"count": 0, "samples": []})
27
+ entry["count"] += 1
28
+ if len(entry["samples"]) < 3:
29
+ entry["samples"].append("%s/%s" % (name, f["name"]))
30
+
31
+ for (family, guid), entry in sorted(seen.items()):
32
+ print("%-16s %-40s %5d %s" % (family, guid, entry["count"],
33
+ ", ".join(entry["samples"])[:90]))