@dforge-core/dforge-mcp 0.2.46-next → 0.2.48

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 (52) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/dist/server.js +71 -3
  3. package/package.json +47 -48
  4. package/resources/docs/dsl-reference.md +1 -1
  5. package/resources/schemas/data-views.schema.json +50 -0
  6. package/skills/dforge-1c-import/SKILL.md +3 -2
  7. package/skills/dforge-1c-import/references/environment.md +2 -1
  8. package/skills/dforge-1c-import/references/loading.md +5 -2
  9. package/skills/dforge-1c-import/references/measurements.md +1 -1
  10. package/skills/dforge-1c-import/scripts/README.md +88 -7
  11. package/skills/dforge-1c-import/scripts/lib/__pycache__/attrs.cpython-313.pyc +0 -0
  12. package/skills/dforge-1c-import/scripts/lib/__pycache__/delta.cpython-313.pyc +0 -0
  13. package/skills/dforge-1c-import/scripts/lib/__pycache__/meta.cpython-313.pyc +0 -0
  14. package/skills/dforge-1c-import/scripts/lib/__pycache__/migrate.cpython-313.pyc +0 -0
  15. package/skills/dforge-1c-import/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
  16. package/skills/dforge-1c-import/scripts/lib/__pycache__/posted.cpython-313.pyc +0 -0
  17. package/skills/dforge-1c-import/scripts/lib/__pycache__/posting.cpython-313.pyc +0 -0
  18. package/skills/dforge-1c-import/scripts/lib/__pycache__/seedstate.cpython-313.pyc +0 -0
  19. package/skills/dforge-1c-import/scripts/lib/__pycache__/store.cpython-313.pyc +0 -0
  20. package/skills/dforge-1c-import/scripts/lib/__pycache__/totals.cpython-313.pyc +0 -0
  21. package/skills/dforge-1c-import/scripts/lib/__pycache__/transactions.cpython-313.pyc +0 -0
  22. package/skills/dforge-1c-import/scripts/lib/attrs.py +2 -2
  23. package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
  24. package/skills/dforge-1c-import/scripts/lib/delta.py +109 -52
  25. package/skills/dforge-1c-import/scripts/lib/docusage.py +5 -4
  26. package/skills/dforge-1c-import/scripts/lib/dump1c.py +249 -92
  27. package/skills/dforge-1c-import/scripts/lib/external.py +4 -3
  28. package/skills/dforge-1c-import/scripts/lib/meta.py +12 -2
  29. package/skills/dforge-1c-import/scripts/lib/migrate.py +93 -74
  30. package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
  31. package/skills/dforge-1c-import/scripts/lib/paths.py +21 -0
  32. package/skills/dforge-1c-import/scripts/lib/peek.py +3 -5
  33. package/skills/dforge-1c-import/scripts/lib/recorded.py +11 -11
  34. package/skills/dforge-1c-import/scripts/lib/recorders.py +9 -12
  35. package/skills/dforge-1c-import/scripts/lib/seedstate.py +21 -16
  36. package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
  37. package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
  38. package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
  39. package/skills/dforge-1c-import/scripts/lib/totals.py +2 -4
  40. package/skills/dforge-1c-import/scripts/lib/transactions.py +28 -0
  41. package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
  42. package/skills/dforge-1c-import/scripts/project-template/sources.py +10 -0
  43. package/skills/dforge-mcp-author/references/action-dsl.md +49 -46
  44. package/skills/dforge-mcp-author/references/data-views.md +38 -2
  45. package/skills/dforge-mcp-author/references/formulas.md +38 -6
  46. package/skills/dforge-mcp-author/references/jobs.md +1 -1
  47. package/skills/dforge-mcp-author/references/print-templates.md +30 -5
  48. package/skills/dforge-mcp-author/references/queries.md +17 -2
  49. package/skills/dforge-mcp-author/references/registers.md +3 -0
  50. package/skills/dforge-mcp-author/references/reports.md +1 -1
  51. package/skills/dforge-mcp-author/references/security.md +2 -0
  52. package/skills/dforge-mcp-author/references/validation-checklist.md +7 -8
package/CHANGELOG.md CHANGED
@@ -6,16 +6,83 @@ the release workflow, so committed `package.json` versions are placeholders.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ **1C migration readiness and regressions.** `delta --apply` now reports per-module
10
+ `ready` / `partial` status and exits 2 if state, totals or imported markers remain
11
+ unsettled; retained retry markers are settled on a later run even without new rows.
12
+ Dry runs do not certify readiness. `pnpm test:1c` covers conversion precision,
13
+ reference/date semantics, failed settlement/retry and the per-entity reload restart
14
+ contract using synthetic fixtures and mocked database boundaries. The loader remains
15
+ a non-atomic migration-target reload; its restart procedure is documented.
16
+ A new CI job (`.github/workflows/onec-tests.yml`) runs it on changes to the 1C scripts or tests. A module's retry
17
+ markers (`df1c.unsettled`) are now cleared only for the entities it settled, not for the whole module.
18
+ Placeholder, balance-state and yearly totals writers roll back failed transactions before
19
+ restoring autocommit; settlement continues to the next module only after connection
20
+ recovery, and failed recovery stops the command. Strict transaction-state tests cover
21
+ original-error preservation, partial yearly commits, continuation and unrecoverable connections.
22
+ Jobs authoring/schema guidance now describes `idempotencyKey` as reserved and not enforced.
23
+
24
+ **Action scripts bypass entity rights.** `action-dsl.md` and `security.md` now state that `insert()` / `update()` /
25
+ `delete()` / `query()` in an action script check neither entity rights nor column security nor the folder's row filter
26
+ (core #1580): only `E` on the action is checked, so treat `E` as write access to everything the script touches.
27
+
28
+ **`canExecute` is enforced in full on the server.** `action.execute` now computes arithmetic, `IN`, `BETWEEN`, formula
29
+ functions, a bare boolean column, `[ref].[col]` navigation and extension columns in SQL, instead of letting them through, and
30
+ `TODAY()` names the requester's day. A condition it cannot compute refuses the action. Install rejects `$[setting]`,
31
+ `SUM` / `COUNT` / `AVG` and unknown function names in `canExecute`, so a module that used them stops installing: read
32
+ settings and count children in `execute:` instead (a count belongs in a `G` column). `references/action-dsl.md`
33
+ (`canExecute:`), `formulas.md` and `validation-checklist.md` are rewritten to match. The button still evaluates over the
34
+ grid row, which has no extension columns and throws on `[ref].[col]`, so keep both out of a gate that must also disable the
35
+ button. **Needs platform 1.28.12.**
36
+
37
+ **Registers: `postAction` / `unpostAction`.** `references/registers.md` documents the register-binding column params that make a direct
38
+ `data.insert` / `data.update` flipping the state flag need `E` on the post or unpost action, and the "May edit is not may
39
+ post" pitfall: declare both and grant `E` only to the roles that may post. Each direction is checked only when its own key
40
+ is set; actions and DSL `update()` are not re-checked. A binding with neither gets a pack / validate warning, silenced by
41
+ `"editorMayPost": true`. **Needs a platform release after 1.28.12 (not in it).**
42
+
43
+ ## 0.2.46 — 2026-10-29
44
+
45
+ Requires `@dforge-core/dforge-cli` 0.2.37 and `@dforge-core/metadata` 0.0.47; the vendored `dforge://schema/data-views` now accepts the grouping keys.
46
+
47
+ **`SLICE()`: an information register's value on a date, per row.** A formula function for `F` columns —
48
+ `SLICE('module.register', 'resource', date, 'dimension', value, …)`, 1C's `СрезПоследних` — so a print form prints each
49
+ line's price on its document's date, read in the query that loads the lines. Database-only (the browser keeps the loaded
50
+ value), read with the user's rights on the register, refused in `G` columns; install checks the register, resource and
51
+ dimensions. `references/formulas.md` documents it, `print-templates.md` and `registers.md` point to it. Print responses
52
+ (`print.render`, `print.renderPdf`, DSL `printPdf()`) now carry `warnings` for what printed empty, and an upgrade deletes
53
+ the print templates a package no longer declares. **Needs a platform with the `SLICE()` release (CLI 0.2.37).**
54
+
55
+ **Grids and lists group by a column.** A `grid` or `list` view's `viewConfig` can ship a default grouping:
56
+ `groupByField`, `groupByOptions` (narrows the Group menu) and `collapsedGroups`, plus `inlineAdd` on a list.
57
+ `dforge_module_validate` checks them as the install does (core `ViewGroupingValidator`, #1556): each named column is a
58
+ dropdown, flags, reference or user column, `groupByOptions` includes `groupByField`, and `collapsedGroups` lists option
59
+ values of a valid `groupByField`. `references/data-views.md` documents the keys and the validation checklist lists the
60
+ rules. The vendored `dforge://schema/data-views` carries them from metadata 0.0.47. **Needs platform 1.28.11.**
61
+
9
62
  **`dforge-1c-import`: porting a 1C infobase.** A fifth skill. It carries the decisions a 1C port turns on, each with the
10
63
  measurement that settles it: a register's kind, where a posting takes each value, what to carry, physical columns through
11
64
  DBNames, vendor against client. It adds `references/loading.md` (copying the rows: swapped uuid halves, the empty reference
12
65
  and the empty date as values in register keys and periods, the year offset, enums by `_EnumOrder`, composite references,
13
66
  register state, triggers, dangling references, verification) and `references/print-forms.md` (MXL templates, the print
14
67
  procedure, `Строка`/`Формат` in SQL, reproducing 1C's slips, datasets as queries). `scripts/` ships the Python toolkit
15
- from a real port: it reads the configuration and DBNames from MS SQL without 1C, generates and validates modules, copies and
68
+ from a real port: it exports the infobase without 1C, generates and validates modules, copies and
16
69
  reconciles the rows, checks postings and reads print templates, pointed at a port by `DF1C_ROOT` / `DF1C_PROJECT` and
17
70
  started from `scripts/project-template/`. `install-skills` installs it with the other four.
18
71
 
72
+ The toolkit has two layers. `dump1c` is the only part that reads 1C. It reads either a server infobase on MS SQL or a
73
+ file infobase (`1Cv8.1CD`, formats 8.2.14 and 8.3.8, through a pure-Python reader), and writes one export to `out/`:
74
+ the configuration, DBNames, the schema, every table's rows as Parquet, and a manifest with the year offset and the
75
+ horizons. Everything else reads that export, with DuckDB for the rows (`store.py`). The export is the same whichever
76
+ kind of infobase it came from, so a project's `sources.QUERIES` are DuckDB SQL. **Breaking for an existing port:** a
77
+ query written in T-SQL (`[_Fld12]`, `TOP`, `0x01`, `CONVERT`) fails until it is rewritten (`"_Fld12"`, `LIMIT`,
78
+ `from_hex('01')`, `CAST`). `dump1c data --changes [--since <day>]` brings an MS SQL export up to date by reading
79
+ only what 1C changed. It rebuilds each table's file in place, so `delta` follows a cutover in minutes rather than
80
+ after a full export. It also catches accounting entries switched on or off without the document being written.
81
+ Register totals and document journals are left out of the export by default; name one, or pass `--derived`, to
82
+ export it. `delta` needs an MS SQL export: a file
83
+ infobase has no database-wide rowversion. `meta` now finds the configuration's root from `root` instead of a GUID
84
+ fixed to one configuration.
85
+
19
86
  ## 0.2.45 — 2026-10-25
20
87
 
21
88
  **English is the base locale, and it lives in the definitions.** Install never reads
package/dist/server.js CHANGED
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
31002
31002
  var fs2 = __toESM(require("fs"));
31003
31003
  var path2 = __toESM(require("path"));
31004
31004
 
31005
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.36/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.37/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31006
31006
  function buildManifest(opts, moduleId) {
31007
31007
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31008
31008
  const manifest = {
@@ -31243,7 +31243,7 @@ function buildZedTasks() {
31243
31243
  var fs = __toESM(require("fs"));
31244
31244
  var path = __toESM(require("path"));
31245
31245
 
31246
- // node_modules/.pnpm/@dforge-core+metadata@0.0.46/node_modules/@dforge-core/metadata/dist/index.js
31246
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.47/node_modules/@dforge-core/metadata/dist/index.js
31247
31247
  var AggType = {
31248
31248
  Sum: "sum",
31249
31249
  Count: "count",
@@ -34612,7 +34612,7 @@ var path10 = __toESM(require("path"));
34612
34612
  var fs7 = __toESM(require("fs"));
34613
34613
  var path9 = __toESM(require("path"));
34614
34614
 
34615
- // node_modules/.pnpm/@dforge-core+metadata@0.0.46/node_modules/@dforge-core/metadata/dist/dsl/index.js
34615
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.47/node_modules/@dforge-core/metadata/dist/dsl/index.js
34616
34616
  var PUNCT3 = ["===", "!=="];
34617
34617
  var PUNCT2 = [
34618
34618
  "==",
@@ -38255,6 +38255,73 @@ function checkMenus(ctx) {
38255
38255
  };
38256
38256
  for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
38257
38257
  }
38258
+ function checkViewGrouping(ctx) {
38259
+ const { err, entities, fieldDefsOf, views } = ctx;
38260
+ for (const [vcode, v] of Object.entries(views)) {
38261
+ const viewType = v.viewType ?? "grid";
38262
+ if (viewType !== "grid" && viewType !== "list") continue;
38263
+ const config2 = v.viewConfig;
38264
+ if (!config2 || typeof config2 !== "object" || Array.isArray(config2)) continue;
38265
+ const cfg = config2;
38266
+ const ent = v.dataSources?.[0]?.entityCode;
38267
+ if (!ent) continue;
38268
+ const defs = fieldDefsOf[ent];
38269
+ if (!defs || entities[ent]?.extends) continue;
38270
+ const where = `data_views \u2192 ${vcode}`;
38271
+ const resolve8 = (key, cd) => {
38272
+ const f = defs[cd];
38273
+ if (!f) err(where, `names '${cd}' in '${key}', which entity '${ent}' does not define`);
38274
+ else if (!isGroupable(f)) err(where, `names '${cd}' in '${key}', which is not a dropdown, reference or user column`);
38275
+ else return f;
38276
+ return void 0;
38277
+ };
38278
+ const readStrings = (key) => {
38279
+ const val = cfg[key];
38280
+ if (!Array.isArray(val)) return void 0;
38281
+ const items = [];
38282
+ for (const item of val) {
38283
+ if (typeof item === "string") items.push(item);
38284
+ else err(where, `has a non-string item in '${key}'`);
38285
+ }
38286
+ return items;
38287
+ };
38288
+ const groupBy = typeof cfg.groupByField === "string" ? cfg.groupByField : void 0;
38289
+ const groupField = groupBy !== void 0 ? resolve8("groupByField", groupBy) : void 0;
38290
+ const options = readStrings("groupByOptions");
38291
+ if (options) {
38292
+ for (const cd of options) resolve8("groupByOptions", cd);
38293
+ if (groupBy !== void 0 && !options.includes(groupBy)) {
38294
+ err(where, `has 'groupByOptions' without its 'groupByField' '${groupBy}'`);
38295
+ }
38296
+ }
38297
+ const collapsed = readStrings("collapsedGroups");
38298
+ if (collapsed) {
38299
+ if (!groupField) err(where, "sets 'collapsedGroups' without a valid 'groupByField'");
38300
+ else if (isDropdown(groupField)) {
38301
+ const values = optionValues(groupField);
38302
+ for (const val of collapsed.filter((c) => !values.has(c))) {
38303
+ err(where, `lists '${val}' in 'collapsedGroups', which is not an option of '${groupBy}'`);
38304
+ }
38305
+ }
38306
+ }
38307
+ }
38308
+ }
38309
+ function isDropdown(f) {
38310
+ return f.fieldTypeCd === "dropdown" || f.fieldTypeCd === "flags";
38311
+ }
38312
+ function isGroupable(f) {
38313
+ return isDropdown(f) || f.fieldTypeCd === "user" || f.columnType === "R";
38314
+ }
38315
+ function optionValues(f) {
38316
+ const result = /* @__PURE__ */ new Set();
38317
+ const options = f.params?.options;
38318
+ if (!Array.isArray(options)) return result;
38319
+ for (const item of options) {
38320
+ const val = item && typeof item === "object" ? item.value : item;
38321
+ if (typeof val === "string" || typeof val === "number") result.add(String(val));
38322
+ }
38323
+ return result;
38324
+ }
38258
38325
 
38259
38326
  // src/tools/validate/security.ts
38260
38327
  var fs14 = __toESM(require("fs"));
@@ -38525,6 +38592,7 @@ var CHECKS = [
38525
38592
  checkEntityViews,
38526
38593
  checkDataViews,
38527
38594
  checkVisibleColumns,
38595
+ checkViewGrouping,
38528
38596
  checkMenus,
38529
38597
  checkRoleRights,
38530
38598
  checkSelectCoverage,
package/package.json CHANGED
@@ -1,49 +1,48 @@
1
1
  {
2
- "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.46-next",
4
- "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
- "license": "MIT",
6
- "homepage": "https://github.com/iash44/dForge-core",
7
- "packageManager": "pnpm@12.14.0+sha512.c18ba6d87993d3d164cb5e8948f32b2c06ee854487e06cd8d86781342fb3e5bb4634367034711e86802baa9c37bea906ce1929ad08d70884c97d801df13389a6",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/dforge-core/dforge-mcp.git"
11
- },
12
- "bin": {
13
- "dforge-mcp": "dist/server.js",
14
- "dforge-install-skills": "scripts/install-skills.mjs"
15
- },
16
- "files": [
17
- "dist/",
18
- "docs/",
19
- "resources/",
20
- "skills/",
21
- "scripts/install-skills.mjs",
22
- "README.md",
23
- "CHANGELOG.md"
24
- ],
25
- "main": "dist/server.js",
26
- "engines": {
27
- "node": ">=18"
28
- },
29
- "scripts": {
30
- "build": "tsup",
31
- "sync-schemas": "node scripts/vendor-schemas.cjs",
32
- "prepublishOnly": "node scripts/vendor-schemas.cjs && tsup",
33
- "typecheck": "tsc --noEmit",
34
- "test": "vitest run",
35
- "install-skills": "node scripts/install-skills.mjs"
36
- },
37
- "dependencies": {
38
- "@dforge-core/dforge-cli": "^0.2.36",
39
- "@dforge-core/metadata": "^0.0.46",
40
- "@modelcontextprotocol/sdk": "^1.30.0",
41
- "zod": "^4.4.3"
42
- },
43
- "devDependencies": {
44
- "@types/node": "^26.2.0",
45
- "tsup": "^8.5.1",
46
- "typescript": "^6.0.3",
47
- "vitest": "^4.1.10"
48
- }
49
- }
2
+ "name": "@dforge-core/dforge-mcp",
3
+ "version": "0.2.48",
4
+ "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/iash44/dForge-core",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dforge-core/dforge-mcp.git"
10
+ },
11
+ "bin": {
12
+ "dforge-mcp": "dist/server.js",
13
+ "dforge-install-skills": "scripts/install-skills.mjs"
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "docs/",
18
+ "resources/",
19
+ "skills/",
20
+ "scripts/install-skills.mjs",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "main": "dist/server.js",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "@dforge-core/dforge-cli": "^0.2.37",
30
+ "@dforge-core/metadata": "^0.0.47",
31
+ "@modelcontextprotocol/sdk": "^1.30.0",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^26.2.0",
36
+ "tsup": "^8.5.1",
37
+ "typescript": "^6.0.3",
38
+ "vitest": "^4.1.10"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "sync-schemas": "node scripts/vendor-schemas.cjs",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "test:1c": "python3 -m unittest discover -s test/onec -v",
46
+ "install-skills": "node scripts/install-skills.mjs"
47
+ }
48
+ }
@@ -569,7 +569,7 @@ if (info.mimeType != 'application/pdf') { error('Receipt must be PDF') }
569
569
  ```
570
570
 
571
571
  #### `printPdf(entityCd, key, templateCd)`
572
- Render a print template to a PDF as the current user and return the archived file as `{ fileId, fileName, mimeType, size, checksum }`; attach it with `sendEmail()`. Pass a qualified entity code. Not in a scheduled job or a background action; max 10 per run.
572
+ Render a print template to a PDF as the current user and return the archived file as `{ fileId, fileName, mimeType, size, checksum }`, plus `warnings` when a value printed empty because the user may not read it; attach it with `sendEmail()`. Pass a qualified entity code. Not in a scheduled job or a background action; max 10 per run.
573
573
 
574
574
  ```dsl
575
575
  execute:
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "additionalProperties": false,
54
54
  "allOf": [
55
+ { "$ref": "#/$defs/rules/grid" },
55
56
  { "$ref": "#/$defs/rules/kanban" },
56
57
  { "$ref": "#/$defs/rules/calendar" },
57
58
  { "$ref": "#/$defs/rules/tree-grid" },
@@ -62,6 +63,17 @@
62
63
  },
63
64
 
64
65
  "rules": {
66
+ "grid": {
67
+ "if": {
68
+ "properties": { "viewType": { "const": "grid" } },
69
+ "required": ["viewType"]
70
+ },
71
+ "then": {
72
+ "properties": {
73
+ "viewConfig": { "$ref": "#/$defs/gridViewConfig" }
74
+ }
75
+ }
76
+ },
65
77
  "kanban": {
66
78
  "if": {
67
79
  "properties": { "viewType": { "const": "kanban" } },
@@ -131,6 +143,26 @@
131
143
  }
132
144
  },
133
145
 
146
+ "gridViewConfig": {
147
+ "type": "object",
148
+ "properties": {
149
+ "groupByField": {
150
+ "type": "string",
151
+ "description": "Dropdown, reference or user column the rows are grouped by under header rows"
152
+ },
153
+ "groupByOptions": {
154
+ "type": "array",
155
+ "items": { "type": "string" },
156
+ "description": "Columns the Group by menu offers; must include groupByField"
157
+ },
158
+ "collapsedGroups": {
159
+ "type": "array",
160
+ "items": { "type": "string" },
161
+ "description": "Option values of groupByField whose groups start collapsed"
162
+ }
163
+ },
164
+ "additionalProperties": false
165
+ },
134
166
  "kanbanViewConfig": {
135
167
  "type": "object",
136
168
  "required": ["groupByField"],
@@ -195,6 +227,24 @@
195
227
  "type": "array",
196
228
  "items": { "$ref": "#/$defs/listLevelConfig" },
197
229
  "description": "Hierarchical child levels for expandable list rows"
230
+ },
231
+ "groupByField": {
232
+ "type": "string",
233
+ "description": "Dropdown, reference or user column the rows are grouped by"
234
+ },
235
+ "groupByOptions": {
236
+ "type": "array",
237
+ "items": { "type": "string" },
238
+ "description": "Columns the user may regroup by; must include groupByField"
239
+ },
240
+ "collapsedGroups": {
241
+ "type": "array",
242
+ "items": { "type": "string" },
243
+ "description": "Option values of groupByField whose groups start collapsed"
244
+ },
245
+ "inlineAdd": {
246
+ "type": "boolean",
247
+ "description": "Show an add row in each group (at the top when ungrouped)"
198
248
  }
199
249
  },
200
250
  "additionalProperties": false
@@ -1,6 +1,6 @@
1
1
  ---
2
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, 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.
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
4
  ---
5
5
 
6
6
  # Porting a 1C infobase to dForge
@@ -341,7 +341,8 @@ judgement above, written down.
341
341
  Do not write the mechanical half again. `scripts/` in this skill's directory holds the
342
342
  Python library that did it on a real port:
343
343
 
344
- - reading the configuration and DBNames straight from MS SQL;
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;
345
346
  - parsing the metadata;
346
347
  - inventorying registers and handlers;
347
348
  - generating and validating modules;
@@ -41,7 +41,8 @@ runs, and every number downstream is confidently wrong.
41
41
 
42
42
  1C shifts stored dates by the infobase's `_YearOffset` — commonly 2000, so `4012` in the
43
43
  column is 2012, and the empty date `0001-01-01` lands on `2001-01-01` and should become
44
- NULL.
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.
45
46
 
46
47
  It applies to **stored datetime columns only**. A date the configuration formatted into a
47
48
  string (`НачалоДня(Дата)` assigned to a `Строка(10)` dimension) carries the real year, and
@@ -47,7 +47,7 @@ Everything else is column-level and mechanical. Write each conversion once, in t
47
47
  |---|---|---|---|
48
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
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. 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. |
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
51
  | 4 | a date 1C formatted into a string (`13.01.2016` in `nvarchar`) | `date` | No offset: nothing stored a datetime there. |
52
52
  | 5 | a number in a string column | numeric | Decimal comma, nbsp or space as the thousands separator. |
53
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. |
@@ -158,4 +158,7 @@ and alter the pairs that disagree. Never narrow one that holds something longer.
158
158
  ## Incremental re-runs
159
159
 
160
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.
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.
@@ -3,7 +3,7 @@
3
3
  Each decision in SKILL.md has a query that settles it. They are written against a loaded
4
4
  dForge tenant (Postgres) because that is where both sides are comparable — the 1C rows and
5
5
  what the platform writes. Where the question can only be asked of the infobase, the query
6
- is against MS SQL and says so.
6
+ is against the export (DuckDB over `out/data/`, via `store.connect()`) and says so.
7
7
 
8
8
  Adapt names; the shapes are the point.
9
9
 
@@ -1,7 +1,8 @@
1
1
  # The 1C porting toolkit
2
2
 
3
- A Python library that reads a 1C:Enterprise 8 infobase hosted on MS SQL Server. It needs
4
- no 1C software and no licence. From the infobase it:
3
+ A Python library that reads a 1C:Enterprise 8 infobase, either a server infobase on MS SQL
4
+ Server or a file infobase (`1Cv8.1CD`). It needs no 1C software and no licence. From the
5
+ infobase it:
5
6
 
6
7
  - generates dForge modules;
7
8
  - copies the rows;
@@ -18,6 +19,55 @@ scripts/
18
19
  project-template/ what a new port starts its project package from
19
20
  ```
20
21
 
22
+ ## Two layers: export, then everything else
23
+
24
+ Only one step reads 1C itself. `dump1c` exports the infobase to `out/`, and every later
25
+ step reads that export, never the infobase.
26
+
27
+ | layer | modules | reads | writes |
28
+ |---|---|---|---|
29
+ | export | `dump1c` · `source_mssql` · `source_file` · `onecd` | the infobase: MS SQL (`DF1C_SERVER`/`DF1C_DB`) or a `.1CD` file (`DF1C_FILE`) | `out/config/`, `dbnames.json`, `schema.json`, `data/<table>.parquet`, `export.json` |
30
+ | everything else | `store` and every other module | `out/`. Rows are queried with DuckDB through `store.connect()` | modules, the tenant, reports |
31
+
32
+ The export looks the same whichever kind of infobase it came from:
33
+
34
+ - names are spelt the MS SQL way (`_Reference12`, `_Fld345RRef`). A file infobase keeps
35
+ them in capitals, and DBNames supplies the spelling back;
36
+ - the schema uses MS SQL type names (`binary(16)`, `nvarchar`, `numeric`, `datetime`);
37
+ - booleans are `binary(1)`.
38
+
39
+ Rows are kept as 1C stores them. `export.json` records the infobase's year offset: usually
40
+ 2000 on MS SQL, always 0 in a file infobase. It also records the horizon (`MIN_ACTIVE_ROWVERSION()`)
41
+ taken before each table was read.
42
+
43
+ - **SQL over the export is DuckDB's.** That includes a project's `sources.QUERIES`. Quote
44
+ names with double quotes (case does not matter), use `LIMIT` instead of `TOP`, write
45
+ a binary constant as `from_hex('01')` instead of `0x01`, and use `CAST` instead of
46
+ `CONVERT`. A port whose queries were written in T-SQL has to rewrite them.
47
+ - **`dump1c data --changes` reads only what 1C changed** since the export (or `--since
48
+ <day>`). It works table by table:
49
+ - objects are re-read by `_Version` above the mark, and deletions are found by key;
50
+ - tabular parts are re-read through their changed owners;
51
+ - register movements are re-read through their changed recorders;
52
+ - anything else is re-read whole.
53
+
54
+ It rewrites each table's file to what a full export would hold. On an infobase of 2.7 M
55
+ objects, it reads the keys in a few seconds instead of a full export of 1.5 h.
56
+
57
+ On an accounting register it also compares each recorder's count of inactive entries.
58
+ Switching entries on or off ("Включить/выключить проводки") rewrites them without
59
+ writing the document, so `_Version` does not show it.
60
+ - **Totals and document journals are left out by default** (`_AccumRgT`, `_AccumRgTn`,
61
+ `_AccRgAT`, `_AccRgCT`, `_DocumentJournal`). 1C derives them from other tables, they
62
+ have no `_Version`, and `--changes` would read them in full on every run: 53.7 M of
63
+ 313 M rows. Name one, or pass `--derived`, to export it. `seedstate verify` says which
64
+ totals table it needs.
65
+ - **`delta` needs an MS SQL export.** In a file infobase `_Version` is a counter per table,
66
+ not a rowversion for the whole database, so a file export has no horizon. Re-export it
67
+ and reload with `migrate run <module> --truncate`.
68
+ - **Read a file infobase while 1C is not writing to it.** Copy the `.1CD` first, or close
69
+ the infobase.
70
+
21
71
  ## Library and project
22
72
 
23
73
  - **`lib/` knows 1C and dForge**: how a configuration is stored, how its metadata
@@ -42,14 +92,17 @@ Then regenerate. If a port keeps needing hand edits, the generator is wrong.
42
92
  ## Setup
43
93
 
44
94
  - Python 3.7+.
45
- - `pyodbc` and an ODBC driver for SQL Server (driver 17 or 18), to read the infobase.
95
+ - `pyarrow` and `duckdb`, to write and read the export (on 3.7: `pyarrow==12.0.1`,
96
+ `duckdb==1.1.3`).
97
+ - `pyodbc` and an ODBC driver for SQL Server (driver 17 or 18), only to export a server
98
+ infobase. A file infobase needs nothing more.
46
99
  - `psycopg2`, to load and check the tenant.
47
100
  - `jsonschema`, for `validate`.
48
101
  - Node with `liquidjs` in `PRINT_NODE_DIR`, only for `printgen preview`.
49
102
 
50
103
  On Windows, set `PYTHONIOENCODING=utf-8`.
51
104
 
52
- Lay out a port directory and point the toolkit at it:
105
+ Lay out a port directory, copy `project-template/.env.example` to `<root>/.env` and fill it in, and export `DF1C_ROOT` / `DF1C_PROJECT` in the shell:
53
106
 
54
107
  ```
55
108
  myport/
@@ -63,6 +116,7 @@ myport/
63
116
  DF1C_ROOT=/path/to/myport
64
117
  DF1C_PROJECT=/path/to/myport/project
65
118
  DF1C_SERVER=<sql server> DF1C_DB=<infobase db> DF1C_USER= DF1C_PASS= # read-only login; Windows auth when USER is empty
119
+ DF1C_FILE=<path to 1Cv8.1CD, or its folder> # a file infobase instead of the two above
66
120
  DF_TENANT=<tenant code> DF_PG_HOST= DF_PG_PORT= DF_PG_USER= DF_PG_PASS= # the tenant's Postgres, for load and checks
67
121
  ```
68
122
 
@@ -76,7 +130,7 @@ docstring with its options, and `python 1c.py --list` lists them all.
76
130
 
77
131
  | # | step | command |
78
132
  |---|---|---|
79
- | 1 | Read the configuration and DBNames from the SQL tables | `dump1c probe` · `dump1c dbnames` · `dump1c schema` · `dump1c extract` |
133
+ | 1 | Export the infobase: configuration, DBNames, schema, rows | `dump1c probe` · `dump1c all`, or one at a time: `dump1c dbnames` · `schema` · `extract` · `data [table ...]` |
80
134
  | 2 | Parse the metadata, resolve type GUIDs, join the physical schema | `meta allobjects` · `typemap` · `analyze` · `attrs <Object>` · `explore <Object>` |
81
135
  | 3 | Extract the module texts (posting handlers, print procedures) | `extract_code` · `unpack <Object> --code` |
82
136
  | 4 | Inventory: what exists, what is used, what is the client's | `inventory` · `docusage` · `custom` · `sizing` · `subsystems` |
@@ -86,7 +140,7 @@ docstring with its options, and `python 1c.py --list` lists them all.
86
140
  | 8 | Install them | `npx -y @dforge-core/dforge-cli@latest module install ...`, or `rebuild --code <tenant> --install` |
87
141
  | 9 | Copy the rows and reconcile | `migrate plan\|check\|run\|verify\|fill <module>` · `integrity` · `columnmap <module>` |
88
142
  | 10 | Post state and registers | `posted [--mark]` · `posting` · `seedstate` · `totals` · `coverage` |
89
- | 11 | Catch up with what 1C changed | `delta --baseline` · `delta [--apply]` |
143
+ | 11 | Catch up with what 1C changed (MS SQL exports only) | once: `dump1c data --objects` (delta needs every object's keys); then `dump1c data --changes`, `delta --baseline` · `delta [--apply]` |
90
144
  | 12 | Print forms and reports | `mxl list [--client]` · `mxl dump <Object>` · `printgen <spec>` · `printgen preview <spec> <form> <key>` · `reportgen <spec>` |
91
145
  | 13 | External reports and processings stored in the infobase | `external` |
92
146
 
@@ -95,7 +149,7 @@ reproducible.
95
149
 
96
150
  ## Rules the toolkit assumes
97
151
 
98
- - **The infobase is read-only.** Use a SELECT-only login.
152
+ - **The infobase is read-only.** Use a SELECT-only login. Read a file infobase from a copy.
99
153
  - **Open ad-hoc database connections in autocommit.** A read transaction left open
100
154
  blocks the next install's `ALTER`.
101
155
  - **Resolve a physical column (`_FldNNN`) only through DBNames** (`attrs`, `columnmap`).
@@ -108,3 +162,30 @@ reproducible.
108
162
  This copy is kept in step with the port it was developed on (`tools/lib`, `tools/1c.py`
109
163
  there). Fixes made in a port go back to the library here. A project's own files never
110
164
  come here.
165
+
166
+ ### Regression checks and restart contract
167
+
168
+ From the MCP repository root: `pnpm test:1c` (Python 3, standard library only).
169
+ These tests use synthetic inputs and mocked database boundaries, never a tenant or
170
+ source infobase. They cover conversions, COPY encoding, settlement failure/retry and
171
+ the initial loader's committed-per-entity restart contract; they do not replace
172
+ Parquet/DuckDB/PostgreSQL integration or real source-to-target reconciliation.
173
+
174
+ `delta --apply` prints each module's `ready` or `partial` status. Exit 0 means all
175
+ selected modules settled; exit 2 means data may have committed but settlement is
176
+ incomplete (state/totals drift or markers not converged, including `--no-mark`
177
+ when markers are needed). Resolve the reported problem and rerun `--apply`;
178
+ persisted unsettled entities are checked even when the new delta has no changes.
179
+ Dry runs report `dry_run` and do not certify readiness. Settlement write failures
180
+ roll back the current transaction before restoring autocommit; ordinary failures
181
+ report `partial` and allow the next module to run on a clean connection. Failure to
182
+ rollback or restore the connection stops the command immediately. Previously
183
+ committed entities/years remain committed; a rollback does not undo the whole import.
184
+ Exit 0 does not replace the wider migration reconciliation.
185
+
186
+ `migrate run --truncate` is a destructive reload into an isolated migration target,
187
+ not an atomic refresh of a live application: clearing commits before loading and
188
+ each entity commits separately. If interrupted, keep users out, repair the cause,
189
+ rerun the same complete selection with `--truncate` from the same export, then
190
+ reconcile again. Do not use an append rerun to recover a partially loaded target.
191
+ For an active target, load/reconcile a separate database and switch only afterward.
@@ -224,8 +224,8 @@ def main():
224
224
  if sys.argv[1] == "--model":
225
225
  out_path, names = sys.argv[2], sys.argv[3:]
226
226
  lines = ["# Transport & shipping contour - extracted model", "",
227
- "Generated from the `%s` MS SQL infobase without 1C software."
228
- % os.environ.get("DF1C_DB", "source"), ""]
227
+ "Generated from the `%s` infobase without 1C software."
228
+ % (os.environ.get("DF1C_DB") or os.path.basename(os.environ.get("DF1C_FILE") or "source")), ""]
229
229
  for name in names:
230
230
  guid, fields = fields_of(name, objects, types)
231
231
  row = report.get(name, {})