@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,375 @@
1
+ ---
2
+ name: dforge-1c-import
3
+ description: How to port a 1C:Enterprise 8 infobase (УТП, УПП, УТ, БП or a configuration built on them) to dForge modules — how to decide a register's kind, where a posting takes each value from, what to carry and what to drop, and how to check every one of those decisions against the data instead of guessing. Use this whenever the work involves a 1C infobase, a 1C configuration, a file infobase (1Cv8.1CD), MS SQL tables named _Reference/_Document/_AccumRg/_InfoRg/_AccRg, регистры, проведение, ОбработкаПроведения, Движения, партионный учёт, or migrating a client off 1C onto dForge — including when the user only says "the client's old system" and the tables turn out to be 1C's, and including when they ask only for a piece of it such as "which register kind should this be" or "why does this document write more rows than 1C does". Also covers copying the rows (keys, dates, enums, composite references, empty values, reconciliation) and porting 1C print forms (MXL templates, Строка/Формат, НСтр) to dForge print templates.
4
+ ---
5
+
6
+ # Porting a 1C infobase to dForge
7
+
8
+ ## The stance
9
+
10
+ A 1C configuration is two things that disagree: what its author **declared** and what its
11
+ documents **do**. The declaration is a proposal, often stale, sometimes never true. The
12
+ movements are the record of what actually happened, thousands of times, for years.
13
+
14
+ So the rule that makes this work tractable: **when a question can be settled by measuring
15
+ the data, measure it.** Not because the configuration lies on purpose, but because a port
16
+ that copies declarations copies the author's mistakes with them, and a port that copies
17
+ behaviour copies the business.
18
+
19
+ Two worked cases, both from a real port, both where the obvious source was wrong:
20
+
21
+ - The accounting policy register pointed at position 0 of a three-value enum and the
22
+ configuration text would not say which value that was. The write-offs said it in five
23
+ independent ways in under an hour.
24
+ - A register dimension declared `Строка(10)` held `13.01.2016` in 35,799 rows. The
25
+ generator read `nvarchar` faithfully — and faithfully carried the author's mistake, a
26
+ date that does not sort, compare, or match anything a posting would write.
27
+
28
+ The corollary matters as much: **the volume of 1C code is not the size of the port.** Most
29
+ of a posting handler is the platform's job in dForge — clearing movements, locking,
30
+ `Свернуть`, balance control, re-posting. What you port is the part that decides *which
31
+ value goes where*, and that is usually a few lines inside a handler of hundreds. Read the
32
+ code to learn the requirement; do not transcribe it. Concretely: extract only the
33
+ value-assignment and computation lines (for example, `ЗаполнитьЗначения` targets and
34
+ arithmetic expressions); do not port loop structure, locking calls, or platform
35
+ bookkeeping code.
36
+
37
+ ## What the tools do, and what you decide
38
+
39
+ Generation and loading are mechanical and should stay that way: unpacking the
40
+ configuration, parsing metadata, mapping types, emitting entities, copying rows,
41
+ reconciling counts. If a port keeps needing hand edits to generated output, the generator
42
+ is wrong, not the output. This skill ships that mechanical half as a toolkit (see **The
43
+ toolkit** below), so a port starts from it rather than from nothing.
44
+
45
+ What no tool can decide:
46
+
47
+ | decision | settled by |
48
+ |---|---|
49
+ | which module an object belongs to | the business, plus the dependency graph |
50
+ | a register's **kind** | measuring the register and reading its writers |
51
+ | where a posting takes each value | where the column physically sits |
52
+ | whether to carry a link at all | who reads it |
53
+ | what a strange column type means | the data in it |
54
+
55
+ The rest of this file is those decisions. Each one has a measurement attached, because a
56
+ decision you can defend with a number survives review and a decision you argued does not.
57
+
58
+ ## Read the posting handler first
59
+
60
+ Reading a handler before porting it is the cheapest move available and it repeatedly
61
+ answers in minutes what the data leaves ambiguous for days. Do it even when you do not
62
+ intend to port that handler.
63
+
64
+ What to extract, in order:
65
+
66
+ 1. **Which registers it writes** (`Движения.<name>`), and for each, whether the register
67
+ belongs to the client or to the vendor configuration. Vendor registers are usually not
68
+ ported; client registers are the porting unit. A register written by **both** is the
69
+ awkward case: measure what fraction of its writes are client-originated before deciding
70
+ anything. A high fraction makes it a client register with vendor writers to account for;
71
+ a low one usually means the register belongs to machinery you are not carrying, and the
72
+ client's few writes into it are a question for the client rather than a port.
73
+ 2. **Where each movement column is filled from.** `ЗаполнитьЗначения(X, "Col")` after
74
+ loading a tabular section means: that column comes from the HEADER while the rest come
75
+ from the lines. This single fact decides the binding (see below) and it is extremely
76
+ common — in the base measured, 24 of 36 register-writing documents do it.
77
+ 3. **Whether it collapses.** `Свернуть(dims, resources)` is not tidying. It means the
78
+ document's lines can repeat a key and the register holds one row per key. Getting this
79
+ wrong writes duplicate keys silently.
80
+ 4. **What a guard protects.** `Если Стр.Количество <> 0` looks like a condition; measuring
81
+ how often it has fired tells you how much it is worth, not whether to keep it. A guard
82
+ that has never fired in years is a poor reason to ask the platform for a new feature —
83
+ but it is not dead code, because future input can still reach it. Keep the condition
84
+ where it is cheap (a column, a computed value, a validation on the target), and drop it
85
+ only when the target genuinely cannot produce the input or the client agrees the rule
86
+ has changed. Record which of those it was.
87
+ 5. **What it computes.** `-(1 - Количество)` is arithmetic, not logic — it becomes a
88
+ computed column, not code.
89
+
90
+ **Size does not measure effort, and using it as a proxy is the trap.** A handler's line
91
+ count is mostly vendor accounting the port does not carry: the largest in the base measured
92
+ here was 1,043 lines writing five registers, four of them the vendor's, and its only
93
+ client-owned movement was a status the port already handled. Its client-relevant part was
94
+ smaller and easier than that of handlers a tenth its size.
95
+
96
+ So scan by size once, to know which handlers you will not read end to end — but do not
97
+ defer the handler itself on that basis, because the client-owned movement buried in a big
98
+ one may be the cheapest thing available. Find the client's registers first and work
99
+ outward from them; order that work by what blocks each handler, so everything unblocked
100
+ ships while the blocked ones wait on one decision each.
101
+
102
+ ### What counts as blocked
103
+
104
+ A handler is **blocked** when something it needs is not yet decided or not yet available,
105
+ and no amount of writing declarations will change that. Everything else — long, tedious,
106
+ unfamiliar — is work, not a block.
107
+
108
+ Name the category and the resolver together, because the resolver is what determines when
109
+ it moves:
110
+
111
+ | category | example | who resolves it |
112
+ |---|---|---|
113
+ | a platform feature that does not exist | a value the binding cannot reach from where it sits | the platform team |
114
+ | a modelling decision not yet made | which register kind, which dimension is missing | you, by measuring |
115
+ | a business question | which valuation method should apply going forward | the client |
116
+ | data that is absent or untrustworthy | a dimension empty across the whole register | investigation, then one of the above |
117
+
118
+ Two habits make this list useful rather than decorative.
119
+
120
+ **Keep a count against each category, and re-examine it.** A blocker recorded once tends to
121
+ stay recorded. In the port this skill comes from, the list shrank three times and every
122
+ time *away* from "we need more platform": a collapse the engine already did, a condition
123
+ that was a column rather than a feature, a register declared as the wrong kind. Expect
124
+ mislabelling in that direction — it is the expensive one, because asking for a platform
125
+ feature costs weeks and re-reading your own declaration costs an hour.
126
+
127
+ **Distinguish "blocked" from "not started".** A handler waiting on a decision is blocked; a
128
+ handler nobody has read is not. The second is much more common early, and conflating them
129
+ makes the port look stuck when it is merely young.
130
+
131
+ ## Choosing a register kind
132
+
133
+ The decision most likely to be made wrong, because the obvious answer — copy the 1C family
134
+ — is wrong often enough to matter. A 1C `РегистрСведений` is not automatically dForge's
135
+ `kind: "info"`.
136
+
137
+ One fact governs everything below: **only `balance` aggregates lines by dimension tuple.**
138
+ `info` and `turnover` keep no state and the engine skips the aggregation for both. So if a
139
+ document's lines can repeat a key, `balance` is the only kind that will fold them.
140
+
141
+ Work through this flow in order. Lines repeat a key when `Свернуть` over the dimensions and
142
+ the measurements (§1) show that repeated document-line tuples become one register row.
143
+ Quantities add; values replace. A price, status, currency, or percentage share is a value;
144
+ a quantity, amount, or count is a quantity.
145
+
146
+ | Step | Question | Answer | Next step / result |
147
+ |---|---|---|---|
148
+ | 1 | Can the document's lines repeat a key? | No | Go to step 2; every kind remains available. |
149
+ | 1 | Can the document's lines repeat a key? | Yes | Go to step 2. Only `balance` can fold them, but that does not select `balance`: a writer can fold its lines and still replace a snapshot. |
150
+ | 2 | Are the resources quantities or values? | Values; lines do not repeat a key | **`info`**. Done. |
151
+ | 2 | Are the resources quantities or values? | Values; lines repeat a key | **Stop: no kind fits.** The key is incomplete: summing values is meaningless. Find the missing dimension. |
152
+ | 2 | Are the resources quantities or values? | Quantities | Go to step 3. |
153
+ | 3 | Do two documents on one key add or replace? Ask the business and the writer's intent; history (§3) only shows whether the case has occurred. | Add | Go to step 4. |
154
+ | 3 | Do two documents on one key add or replace? | Replace; lines do not repeat a key | **`info`**. Done. |
155
+ | 3 | Do two documents on one key add or replace? | Replace; lines repeat a key | **Stop: no kind fits.** Three options, none free — see the note below. |
156
+ | 4 | Is there a running total to keep? | Yes | **`balance`**. |
157
+ | 4 | Is there a running total to keep? | No; lines do not repeat a key | **`turnover`**: it keeps no total and answers only over an interval. |
158
+ | 4 | Is there a running total to keep? | No; lines repeat a key | **`balance`**: accept the running total because `turnover` would write one row per line. |
159
+
160
+ **The step 3 "replace, lines repeat" note.** Three options, and the choice belongs in the
161
+ declaration's comment along with the reason: declare `balance` and accept that it adds
162
+ across documents; collapse before posting, which nothing expresses declaratively today; or
163
+ leave it unbound and record it as blocked. Taking the first means **enforcing the
164
+ assumption** — a unique constraint over the dimensions on the state entity, so a second
165
+ writer fails loudly instead of summing. Where you cannot enforce it, say in the comment
166
+ that the semantics are "replace" and the kind is a compromise; the next reader will ask,
167
+ and the honest answer is shorter than the reconstruction.
168
+
169
+ Past uniqueness does not make adding safe: a future correction can otherwise double a
170
+ period the first time someone re-states it.
171
+
172
+ One aside that trips people: `info` with a `recorder` is the shape for a 1C register
173
+ subordinate to a регистратор. But subordination is about *who wrote the row*, not about how
174
+ rows combine — do not let it answer step 1.
175
+
176
+ ### When the handler cannot be read
177
+
178
+ Extraction can come up short, the source can be missing for an object, and a heavily
179
+ customised configuration can defeat a parser that works everywhere else. None of the four
180
+ steps actually depends on the code: the code was only ever the fastest way to see the
181
+ answer, and the rows carry the same answer more slowly.
182
+
183
+ | step | without the handler |
184
+ |---|---|
185
+ | 1. lines repeat a key? | compare line counts per document against the rows the register holds for that same document — fewer rows than lines **is** the collapse |
186
+ | 2. quantities or values? | the column types and what the values do over time: a figure that repeats unchanged across documents is a value, one that varies per movement is a quantity |
187
+ | 3. add or replace? | history says only whether it has been exercised, so this one still goes to the business — the code would not have answered it either |
188
+ | 4. running total? | whether the register carries a closing or on-hand figure at all, and whether anything reads it as of a date |
189
+
190
+ Two cautions. A missing handler is worth a moment's suspicion before you route around it:
191
+ if the extraction silently dropped it, other objects are missing too, and finding that out
192
+ now is cheaper than discovering it three registers later. And when you infer rather than
193
+ read, say so in the declaration's comment — an inference that later turns out wrong is much
194
+ easier to unpick when it is labelled as one.
195
+
196
+ ## Where a posting takes each value
197
+
198
+ A binding that posts from lines reads its values off the line. Any value that belongs to
199
+ the document goes in `headerMap` beside `map`:
200
+
201
+ ```json
202
+ "map": { "product_id": "product_id", "quantity": "quantity" },
203
+ "headerMap": { "dealer_id": "dealer_id", "period_id": "period_id" },
204
+ "lockedFields": ["dealer_id", "period_id"]
205
+ ```
206
+
207
+ It is explicit rather than a fallback, and that is deliberate: "not on the line, try the
208
+ header" turns a typo into a silent read of an unrelated column, and a dimension that reads
209
+ as null is how a line says it does not participate — so the document would post fewer rows
210
+ than it has lines and say nothing.
211
+
212
+ Two rules that bite:
213
+
214
+ - **Every `headerMap` column must be in `lockedFields`.** The posting is keyed by it.
215
+ Change it under a posted document and reversal looks for a tuple that was never posted
216
+ to, leaving the old row carrying the quantity forever.
217
+ - **A resource may come from the document only on an `info` register.** Anywhere else four
218
+ lines would add the document's amount four times.
219
+
220
+ `headerMap` gives the column **as it stands**. If the 1C handler wrote `НачалоДня(Дата)`
221
+ and the header column is a timestamp, the port needs a day-truncated computed column —
222
+ check whether the time is really there rather than assuming either way, and check whether
223
+ the document already carries a date-typed column that says the same thing.
224
+
225
+ ## Whether to carry something at all
226
+
227
+ Before porting a column, a link or a mechanism, ask **who reads it**. This is a grep, and
228
+ it settles arguments that otherwise run on aesthetics.
229
+
230
+ A worked case: a register was subordinate to a регистратор, so every one of its 322,352
231
+ rows knew its document. Carrying that into dForge would have meant declaring a journal.
232
+ The grep found that the only object in the entire infobase naming that register was the
233
+ document that writes it, and the only appearance of the recorder was
234
+ `УдалитьДвиженияРегистратора` — which is unposting, which the platform does on its own. No
235
+ reader, no journal.
236
+
237
+ The same question retires whole categories. A column nothing writes and nothing reads is
238
+ worse than absent, because the next reader assumes it means something.
239
+
240
+ ## Identify physical columns from metadata, never from cardinality
241
+
242
+ 1C's SQL columns are `_FldNNN`. The temptation, when the metadata is awkward to parse, is
243
+ to identify them by how they behave: the dimension with 86,000 distinct values "must be"
244
+ the batch, the one with 40 "must be" the warehouse. **Do not.** The configuration keys
245
+ every attribute by GUID and `DBNames` maps that GUID to its `_FldNNN`; resolve them that
246
+ way, and use cardinality only as a sanity check on the result.
247
+
248
+ This is not a style preference. In the port this skill comes from, a cardinality guess
249
+ swapped `СерияНоменклатуры` for `ДокументОприходования` — two reference dimensions of the
250
+ same shape — and five measurements were then computed confidently against the wrong
251
+ column, published, and had to be withdrawn. The failure is silent by construction: every
252
+ query still runs, every number still looks plausible, and nothing downstream objects.
253
+
254
+ The re-measurement also found what the guess had hidden: the real batch dimension was
255
+ **empty on all 13.8 million rows**, which is a much larger fact about the client's
256
+ accounting than anything the wrong column could have said.
257
+
258
+ ## What a strange column type means
259
+
260
+ 1C's physical types are not the author's intent, and the port reads the physical type. Two
261
+ failure modes, both real:
262
+
263
+ - **A value parked in the wrong type.** A date in `nvarchar`, a number in a string column.
264
+ 1C coerces silently, so it looks fine in 1C and is useless afterwards. Sweep for it once
265
+ rather than meeting it one column at a time:
266
+
267
+ ```sql
268
+ -- every text column holding something shaped like a date
269
+ select '<schema>.<table>.<col>', count(*) from <schema>.<table>
270
+ where <col> ~ '^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$'
271
+ ```
272
+
273
+ - **An empty string that is not empty.** 1C writes `''` where a value is absent, so
274
+ `count(column)` counts it and a column the infobase never filled reads as completely
275
+ full. Every emptiness check against 1C-sourced data must test for `''`, not `NULL`.
276
+
277
+ When you retype a column, remember that a date the configuration formatted into a string
278
+ is **not** a 1C date value and must not get the `_YearOffset` correction — that offset is
279
+ how 1C stores a datetime, and nothing stored a datetime there.
280
+
281
+ ## Vendor and client
282
+
283
+ Configurations built on a vendor's (УТП, УПП, УТ) carry far more code than the client
284
+ wrote. Separate them early — the integrator's own objects almost always carry a lowercase
285
+ prefix before a capital (`мсбЗаказ`, `рзТранспорт`), and everything else is the vendor's.
286
+ If no such convention exists or it is applied inconsistently, do not fall back to "what the
287
+ client's documents touch" — that is circular, since which documents are the client's is the
288
+ question. Compare against the vendor's stock configuration instead: an object absent from
289
+ it is the integrator's, and an object present but altered is a modification worth reading
290
+ before porting either version of it.
291
+
292
+ **That comparison is often unavailable, and saying so is part of the answer.** The client
293
+ may not hold the distribution, the version they run may not be the version you can get, and
294
+ obtaining it can require exactly the licence this approach is designed not to need. When
295
+ the stock configuration is missing or its version does not match, record the mismatch
296
+ explicitly and treat every conclusion that rests on the comparison as provisional until
297
+ something measured confirms it.
298
+
299
+ Signals that need no stock configuration, in rough order of strength: the naming
300
+ convention; whether an object holds any data at all (a stock object the client never used
301
+ is not a porting question); which objects the extracted code actually writes to; and the
302
+ language and style of the names themselves. None of these is conclusive alone, which is why
303
+ agreement between several of them is what you are looking for.
304
+
305
+ **When they disagree, treat the object as the client's until something settles it.** The
306
+ two mistakes are not symmetric: porting a vendor object you did not need costs some work
307
+ that can be dropped later, while dropping a client object loses business logic silently and
308
+ is found months on, by someone who cannot reconstruct it. An object with no prefix but real
309
+ data and real writers is exactly the case this rule exists for — and it is common, because
310
+ integrators abandon their own conventions under deadline.
311
+
312
+ Two things follow:
313
+
314
+ - **Port the client's requirements, not the vendor's metadata structure.** The stock
315
+ configuration is the vendor's intellectual property; what it encodes about the client's
316
+ business is what you are after.
317
+ - **Do not assume vendor machinery is unused.** Measure which of the client's own objects
318
+ touch it. A register that looks like pure vendor plumbing may be written by twenty of
319
+ the client's documents, and dropping it silently removes what those documents are for.
320
+
321
+ ## Order of work
322
+
323
+ 1. Extract and inventory: what exists, how many rows, what is the client's.
324
+ 2. Generate and load the plain entities. Reconcile counts and control sums against the
325
+ infobase before going further — a port that cannot reproduce a count cannot be trusted
326
+ about anything harder. If counts or control sums do not reconcile, stop and diagnose the
327
+ generator or loader before proceeding to any handler work; do not proceed with partial
328
+ or approximate data.
329
+ 3. Read the posting handlers. Triage by size, then order the work by what blocks each one.
330
+ 4. Declare registers and bind documents, starting where nothing blocks.
331
+ 5. Keep a running list of what blocks the rest, with a count against each. The list should
332
+ shrink in both directions: things the platform must gain, and things that turn out to
333
+ have been modelled wrong. Expect the second to be more common than it feels.
334
+
335
+ Keep the generated half and the project half separated by a check, not by convention. The
336
+ generic half is most of the code and lifts to the next infobase; the project half is the
337
+ judgement above, written down.
338
+
339
+ ## The toolkit
340
+
341
+ Do not write the mechanical half again. `scripts/` in this skill's directory holds the
342
+ Python library that did it on a real port:
343
+
344
+ - exporting the infobase (configuration, DBNames, schema, rows) to `out/`, from MS SQL or
345
+ from a file infobase (`1Cv8.1CD`). Every later step reads the export, never 1C;
346
+ - parsing the metadata;
347
+ - inventorying registers and handlers;
348
+ - generating and validating modules;
349
+ - copying rows with every conversion in the loader;
350
+ - reconciling counts, sums and foreign keys;
351
+ - posting checks;
352
+ - reading MXL print templates.
353
+
354
+ `scripts/README.md` gives the setup and the pipeline step by step.
355
+
356
+ Start a port by copying `scripts/project-template/` into the port's own directory. Set
357
+ `DF1C_ROOT` and `DF1C_PROJECT`, then run `python <skill>/scripts/1c.py --list`. Everything
358
+ this infobase needs goes into the project package, never into `lib/`; `1c.py --boundary`
359
+ checks that. When a tool gets something wrong for a new infobase, fix the library and keep
360
+ the fix general. A second infobase is what turns a port's tooling into a library.
361
+
362
+ ## References
363
+
364
+ - `references/measurements.md` — the queries behind each decision, ready to adapt
365
+ - `references/loading.md` — copying the rows: the conversions (keys, empty values, the
366
+ year offset, enums, composite references), register state, triggers, dangling
367
+ references, verification, incremental re-runs
368
+ - `references/print-forms.md` — porting 1C print forms: reading MXL templates, what the
369
+ print procedure decides, reproducing 1C's formatting and its slips, datasets as
370
+ queries, verifying a form
371
+ - `references/environment.md` — the environment traps that cost time repeatedly (Windows
372
+ bash, Cyrillic in shell commands, 1C physical naming, the year offset)
373
+ - Platform side, from the dForge MCP server: `dforge://reference/registers`,
374
+ `dforge://reference/data-migration`, `dforge://reference/print-templates`,
375
+ `dforge://reference/queries`
@@ -0,0 +1,98 @@
1
+ # Environment traps
2
+
3
+ The things that cost hours the first time and minutes every time after. Written down so
4
+ they cost minutes the first time.
5
+
6
+ ## 1C physical storage
7
+
8
+ The infobase's SQL tables carry generated names; the configuration maps GUIDs to them.
9
+
10
+ | prefix | what it is |
11
+ |---|---|
12
+ | `_Reference<N>` | catalog (справочник) |
13
+ | `_Document<N>` | document; its tabular sections are `_Document<N>_VT<M>` |
14
+ | `_InfoRg<N>` | information register; `_InfoRgSL<N>` is its slice-last cache |
15
+ | `_AccumRg<N>` | accumulation register; `_AccumRgT<N>` its totals, `_AccumRgTn<N>` turnovers |
16
+ | `_AccRg<N>` | accounting register |
17
+ | `_Enum<N>` | enumeration; values carry `_EnumOrder`, never their names |
18
+ | `_*ChngR<N>` | change registration for exchange — not the object's own data |
19
+
20
+ Common columns: `_IDRRef` (16-byte GUID key), `_Period`, `_Active`, `_RecordKind`
21
+ (0 receipt / 1 expense), `_RecorderRRef` + `_RecorderTRef` (the writing document, typed),
22
+ `_LineNo`, `_Fld<N>` for every declared field in declaration order — dimensions first,
23
+ then resources, then attributes.
24
+
25
+ A field ending `RRef` is a single-type reference; a `_TYPE`/`_RTRef`/`_RRRef` triple is a
26
+ composite one. `_IDRRef` bytes are stored with the halves swapped relative to the printed
27
+ GUID.
28
+
29
+ **Resolving a `_Fld` number to a name goes through `DBNames`, not through guessing.** The
30
+ configuration keys every attribute by GUID, and `DBNames` holds a `Fld` entry per GUID
31
+ giving its `_FldNNN`; the same file maps `VT` (a tabular section's table) and `LineNo`.
32
+ Parse the object, collect its attributes with their GUIDs, and look each one up. Automate
33
+ it once — everything else depends on it being right.
34
+
35
+ Field numbers do run in declaration order from the object's base number, dimensions first,
36
+ and distinct-value counts do look suggestive. Use that only to **check** a resolution you
37
+ already made: two reference dimensions of similar shape swap silently, every query still
38
+ runs, and every number downstream is confidently wrong.
39
+
40
+ ## The year offset
41
+
42
+ 1C shifts stored dates by the infobase's `_YearOffset` — commonly 2000, so `4012` in the
43
+ column is 2012, and the empty date `0001-01-01` lands on `2001-01-01` and should become
44
+ NULL. That is a server infobase. A file infobase stores real dates (offset 0). The export
45
+ records which applies in `export.json`, and `store.year_offset()` reads it from there.
46
+
47
+ It applies to **stored datetime columns only**. A date the configuration formatted into a
48
+ string (`НачалоДня(Дата)` assigned to a `Строка(10)` dimension) carries the real year, and
49
+ shifting it as well moves the value back two millennia.
50
+
51
+ ## An empty string is not NULL
52
+
53
+ 1C writes `''` where a value is absent. So `count(column)` counts it, and a column the
54
+ infobase never filled reads as completely full. Sweep for text columns blank on every row
55
+ before trusting any of them; the results split into stock fields the client does not use
56
+ (fine) and columns the port read from the wrong source (not fine).
57
+
58
+ ## Shell: Cyrillic and backticks
59
+
60
+ When commands are wrapped for execution, Cyrillic characters and backticks in the command
61
+ text can break the wrapper before the command runs — the symptom is `command not found`
62
+ for fragments of your own script, or a syntax error near a Cyrillic word.
63
+
64
+ The fix that always works: write the script to a file with a file-writing tool, then run
65
+ it by an ASCII-only path. Inside the file, Cyrillic is fine; in a pinch, `\uXXXX` escapes
66
+ in the source keep the command itself ASCII.
67
+
68
+ ## Windows: `bash` is not the bash you want
69
+
70
+ On Windows, `bash` on PATH resolves to WSL's, which lives in a different filesystem and
71
+ fails with `execvpe(/bin/bash) failed` before reaching your script. Anything that shells
72
+ out to a `.sh` needs Git's bash by full path:
73
+
74
+ ```
75
+ C:\Program Files\Git\bin\bash.exe
76
+ ```
77
+
78
+ This is invisible when working interactively from Git Bash and appears the moment a script
79
+ does the same thing.
80
+
81
+ ## Python on Windows
82
+
83
+ Output encoding defaults to the system codepage, so Cyrillic output raises
84
+ `UnicodeEncodeError`. Set `PYTHONIOENCODING=utf-8`. When reading and writing files, name
85
+ the encoding explicitly (`io.open(path, encoding="utf-8")`) rather than relying on the
86
+ default.
87
+
88
+ ## Reading the infobase
89
+
90
+ Read-only, always. Porting needs no writes to the source, and a `SELECT`-only credential
91
+ makes that a property of the connection rather than a promise.
92
+
93
+ No 1C licence is engaged by reading the SQL tables directly, which is what makes this
94
+ approach available at all — but it also means nothing validates your reading of the
95
+ metadata except the data itself.
96
+
97
+ Guard the size of exploratory queries: a register of 14 million rows will answer a
98
+ well-scoped question about one month in seconds and an unscoped one in minutes.
@@ -0,0 +1,164 @@
1
+ # Copying the rows
2
+
3
+ How the infobase's data gets into a dForge tenant once the modules are installed. The
4
+ general shape of a migration script is in `dforge://reference/data-migration`. This file
5
+ covers what is specific to 1C.
6
+
7
+ ## Copy, don't replay
8
+
9
+ There are two routes:
10
+
11
+ - **Through the API** (`data.set`, CSV import). Every document is re-posted on the way in.
12
+ - **Straight into Postgres** with `COPY`.
13
+
14
+ Take the second for the history. Re-posting needs the posting logic ported first. Once it
15
+ is ported, its arithmetic has to match 1C's to the kopeck, or the imported balances differ
16
+ from the ones the client reconciles against. It is also slow: millions of documents
17
+ through RPC take days.
18
+
19
+ A direct copy keeps the values 1C computed and runs at disk speed. The API route stays
20
+ useful for one job: catching up the last days at cutover, while 1C is still live.
21
+
22
+ ## Shape the modules so the rows fit
23
+
24
+ A copy is only mechanical if nothing has to be reshaped:
25
+
26
+ - **Use the infobase's key as the key.** Key every entity by `_IDRRef` as a `uuid`. Then no
27
+ old-key → new-key table is needed and no reference has to be re-resolved.
28
+ - **Line tables keep 1C's composite key**, `(<owner>_id, line_no)`, as
29
+ `_Document<N>_VT<M>` is keyed.
30
+ - **Port the reference data as well.** Mirror `Номенклатура`, `Контрагенты`, `Склады`
31
+ rather than mapping them onto platform modules. Then a line's `product_id` is the same
32
+ 16 bytes on both sides.
33
+ - **Build the column map as the entities are generated**, not afterwards, so it cannot
34
+ drift from the schema. The map records which `_FldNNN` feeds each target column.
35
+ Authored entities, whose names were chosen by hand, get a map written by hand. Propose
36
+ it by running each 1C attribute name through the same naming function and declare only
37
+ the leftovers.
38
+ - **Read the target's physical shape from the tenant itself** (`"dForge".entity`,
39
+ `information_schema`). Do not read it from the module files. The loader then cannot
40
+ disagree with what the installer built.
41
+
42
+ ## The conversions
43
+
44
+ Everything else is column-level and mechanical. Write each conversion once, in the loader.
45
+
46
+ | # | 1C storage | becomes | the trap |
47
+ |---|---|---|---|
48
+ | 1 | `_IDRRef binary(16)` | `uuid` | 1C stores the two 8-byte halves swapped relative to the printed GUID. In SQL: `substr(h,17)\|\|substr(h,1,16)` over the hex. Check it once by joining a line to its catalog and seeing the expected name. |
49
+ | 2 | sixteen zero bytes | `NULL` in a link | Left as-is, it is a uuid pointing at nothing, and the FK rejects the load. **In a register dimension it is a value, not an absence:** keep it as the nil uuid in a key column, because NULL ≠ NULL in a balance, a GROUP BY or a unique key. |
50
+ | 3 | `datetime` | date/timestamp minus `_YearOffset` | The offset is commonly 2000 on MS SQL, and 0 in a file infobase (`export.json` says which). The empty date `0001-01-01` lands on `2001-01-01` and becomes NULL. **On an information register's period it is a value:** "in force since the beginning". Load it as NULL and the slice (`period <= @date`) never finds the row. |
51
+ | 4 | a date 1C formatted into a string (`13.01.2016` in `nvarchar`) | `date` | No offset: nothing stored a datetime there. |
52
+ | 5 | a number in a string column | numeric | Decimal comma, nbsp or space as the thousands separator. |
53
+ | 6 | `binary(1)` flags | `bool` | `_Marked`, `_Posted`, `_Folder`. Check `_Folder`'s polarity against a known group and a known item. Do not trust the name. |
54
+ | 7 | enum reference | option code | The value is a GUID into `_Enum<N>`. `_EnumOrder` is its position in the metadata, so read the options in that order and pair by position. Generate the dropdown from the enum: hand-written option lists come out short. |
55
+ | 8 | composite reference: `_TYPE` + `_RTRef` + `_RRRef` | the key, plus a "which table" column where the port keeps one | The table number in `_RTRef` is the only thing that says what the row on the other end is. Map numbers to meanings via DBNames. |
56
+ | 9 | `ХранилищеЗначения` (`image`) | `bytea`, copied as is | It declares itself a reference. Read as one, it becomes a uuid column that can never be filled. |
57
+ | 10 | `nchar` padding | trimmed text | `_Code nchar(N)` shows trailing spaces. |
58
+ | 11 | `''` | `''` or NULL, decided once | 1C writes `''` for "no value". Every emptiness check must test both. |
59
+
60
+ ### Register keys
61
+
62
+ An information register has no key in 1C. Where the infobase kept one, it is `_SimpleKey`.
63
+ For the rest, derive the key from the row: a uuid5 over the source table and the row's
64
+ values. Then a re-run lands on the same key and a reload is idempotent.
65
+
66
+ ### Movement direction
67
+
68
+ 1C's convention depends on the register kind:
69
+
70
+ - **Balance register.** Resources are unsigned. `_RecordKind` says which way the row went:
71
+ 0 is a receipt (+1), 1 is an expense (−1). Refuse anything else; never default it.
72
+ - **Turnover register.** Some authors kept the direction in the sign of the resource. Some
73
+ registers carry the two sides as separate resources, so every row is additive.
74
+
75
+ Read which convention applies from the writer. Then fill the platform's direction stamp
76
+ in the loader, not through a column default. An upgrade can arrive without the default,
77
+ and every `SUM(value * direction)` silently becomes NULL.
78
+
79
+ ## What goes where for registers
80
+
81
+ In 1C, posting writes movements to `_AccumRg<N>`, and `_AccumRgT<N>` caches period totals.
82
+ In dForge, a register is an `A` column on the document plus a balance entity. In line
83
+ mode the document's lines are the detail. So:
84
+
85
+ - **Copy the documents and their lines.** `_Posted` becomes the register column's state.
86
+ - **Copy the balances** from `_AccumRgT<N>`, or aggregate them from the movements. They are
87
+ 1C's numbers, and those are what the client reconciles against.
88
+ - **Do not copy line-mode movement rows.** They have nowhere to go: the lines are the detail.
89
+ - **Close every period up to cutover after the load.** Unposting an imported document
90
+ recomputes its reversal from the lines. Where 1C's handler did more than sum the lines
91
+ (batch write-off, distribution), that reversal would not match what 1C wrote. The close
92
+ guard then refuses edit, unpost and delete on history.
93
+
94
+ A raw `UPDATE` of a posted flag is refused (`DF006 REGISTRY_STATE_DIRECT`). Posting state
95
+ is the engine's.
96
+
97
+ ## Order and triggers
98
+
99
+ Postgres fires row triggers on `COPY`. The period lock and the posted-document guards
100
+ would reject exactly the rows a migration writes. So:
101
+
102
+ 1. Load with `SET session_replication_role = 'replica'`.
103
+ 2. Load in dependency order: catalogs, then documents, then lines, then balances.
104
+ 3. Load self-referencing catalogs (parent hierarchies) in one pass with the constraint
105
+ suspended. A topological sort buys nothing.
106
+ 4. Re-enable the triggers, close the periods, verify.
107
+
108
+ Keep ad-hoc measurement connections in **autocommit**. A long read transaction left open in
109
+ a console holds a lock that blocks the next install's `ALTER` for as long as it stays open.
110
+
111
+ ## Dangling references
112
+
113
+ 1C does not enforce referential integrity. A copy arrives with references to rows deleted
114
+ years ago. Prove it on the 1C side first: it is the infobase's state, not a load bug.
115
+ There are three ways out:
116
+
117
+ - **Null the reference.** The row stops saying what it said.
118
+ - **Drop the FK.** You give up the integrity the port exists to gain.
119
+ - **Create the missing row and mark it.** Give every ported catalog a read-only
120
+ `is_missing` flag. After a contour loads, walk the FKs and insert one flagged row per
121
+ missing key, its text columns carrying the key so it is traceable. **Recommended:** it
122
+ is the only option that keeps everything.
123
+
124
+ Verification then counts `WHERE NOT is_missing`.
125
+
126
+ ## Verifying the load
127
+
128
+ These checks are all cheap, and all should be automated from the first contour:
129
+
130
+ - **Row counts** per source table against the target.
131
+ - **Control sums** on every resource and every money column, 1C against Postgres. This is
132
+ the check that catches a decimal comma or a missed offset.
133
+ - **Reference integrity.** Ask `pg_constraint`, not your own metadata: count every
134
+ single-column FK against its target, and compare every FK with the reference column's
135
+ declared target. A check that reads the same metadata the generator wrote agrees with
136
+ it by construction. A retargeted reference can keep its old FK for months.
137
+ - **A sample of joins** resolving to the name a 1C report shows.
138
+
139
+ Before the first load, ask the data what the specs claim: that a column is mandatory,
140
+ wide enough, unique, and holds the declared kind. Do it in one pass, not one failed
141
+ `COPY` at a time. The answers are usually the port being tidier than the business. For
142
+ example, a "mandatory" kind may be empty on 95% of rows, and a "unique" plate number
143
+ repeat after years of re-registrations.
144
+
145
+ ## Upgrades leave the physical table behind
146
+
147
+ `module upgrade` can rewrite the metadata and leave the table as it was. Check this after
148
+ any upgrade that changed a column:
149
+
150
+ - a widened `varchar` still narrow;
151
+ - a retargeted reference keeping its old FK;
152
+ - a removed unique constraint still in place;
153
+ - a type change not applied.
154
+
155
+ The repair for widths is one query: join `entity_column` to `information_schema.columns`
156
+ and alter the pairs that disagree. Never narrow one that holds something longer.
157
+
158
+ ## Incremental re-runs
159
+
160
+ 1C's `_Version` (rowversion) gives a cheap delta. Take the high-water mark **before** the
161
+ read, so a row 1C changes during the read is picked up next time rather than lost. The
162
+ export takes it before each table and records it in `export.json`. Only a server infobase
163
+ has one. In a file infobase `_Version` is a counter per table, so a file infobase is
164
+ reloaded whole, which its size allows.